diff --git a/.changeset/reduce-api-to-a-minimum.md b/.changeset/reduce-api-to-a-minimum.md new file mode 100644 index 000000000..958c893c9 --- /dev/null +++ b/.changeset/reduce-api-to-a-minimum.md @@ -0,0 +1,10 @@ +--- +"braintrust": major +"@braintrust/otel": major +"@braintrust/vercel-ai-sdk": major +"@braintrust/browser": patch +"@braintrust/openai-agents": patch +"@braintrust/temporal": patch +--- + +ref!: Reduce API to a minimum that makes sense diff --git a/e2e/package.json b/e2e/package.json index 8de220b7e..414b75247 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -8,6 +8,7 @@ "test:e2e:record": "node ./scripts/run-record-tests.mjs" }, "devDependencies": { + "@braintrust/browser": "workspace:^", "@braintrust/langchain-js": "workspace:^", "@braintrust/otel": "workspace:^", "@braintrust/seinfeld": "workspace:^", diff --git a/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts b/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts index ca68896e5..0e75a3319 100644 --- a/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts +++ b/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts @@ -7,9 +7,7 @@ const agentsPackageName = const instrumentationMode = process.env.CLOUDFLARE_AGENTS_INSTRUMENTATION_MODE ?? "auto"; const braintrustPlugins = - instrumentationMode === "auto" - ? braintrustVitePlugin({ useDiagnosticChannelCompatShim: true }) - : []; + instrumentationMode === "auto" ? braintrustVitePlugin({ browser: true }) : []; export default defineConfig({ define: { diff --git a/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts b/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts index 3952e5fd6..1c131a62d 100644 --- a/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts +++ b/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts @@ -25,9 +25,7 @@ export default defineConfig({ __OPENAI_BASE_URL__: JSON.stringify(process.env.OPENAI_BASE_URL), }, plugins: [ - ...(mode === "auto" - ? [braintrustVitePlugin({ useDiagnosticChannelCompatShim: true })] - : []), + ...(mode === "auto" ? [braintrustVitePlugin({ browser: true })] : []), cloudflare({ configPath: "./wrangler.toml", inspectorPort: 0 }), ], resolve: { diff --git a/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts b/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts index 9a28d8c50..d068998ef 100644 --- a/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts +++ b/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts @@ -13,9 +13,7 @@ export default defineConfig({ ), }, plugins: [ - ...(manual - ? [] - : [braintrustVitePlugin({ useDiagnosticChannelCompatShim: true })]), + ...(manual ? [] : [braintrustVitePlugin({ browser: true })]), cloudflare({ inspectorPort: 0 }), ], resolve: { diff --git a/e2e/scenarios/deno-browser/__snapshots__/request-flow.json b/e2e/scenarios/deno-browser/__snapshots__/request-flow.json index f8eea5a3f..ca74c4553 100644 --- a/e2e/scenarios/deno-browser/__snapshots__/request-flow.json +++ b/e2e/scenarios/deno-browser/__snapshots__/request-flow.json @@ -27,7 +27,11 @@ "api_version": 2, "rows": [ { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "expected": "Paris", "id": "", @@ -54,7 +58,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -89,7 +97,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -120,7 +132,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -142,18 +158,24 @@ "phase": "child" }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 3, - "name": "deno browser child span", - "type": "task" + "name": "deno browser child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-parent", @@ -165,18 +187,22 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 4, "name": "deno browser nested parent span", "type": "task" }, - "span_id": "" + "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-child", @@ -188,18 +214,24 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 5, - "name": "deno browser nested child span", - "type": "task" + "name": "deno browser nested child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-grandchild", @@ -210,19 +242,28 @@ "end": 0, "start": 0 }, + "output": { + "depth": 3 + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 6, - "name": "deno browser nested grandchild span", - "type": "task" + "name": "deno browser nested grandchild span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "current-span", @@ -233,14 +274,17 @@ "end": 0, "start": 0 }, + "output": { + "observedSpanId": "" + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 7, "name": "deno browser current span", "type": "task" }, - "span_id": "" + "span_id": "" } ] }, @@ -251,7 +295,11 @@ "api_version": 2, "rows": [ { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "expected": "Paris", "id": "", @@ -278,7 +326,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -313,7 +365,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -344,7 +400,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -366,18 +426,24 @@ "phase": "child" }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 3, - "name": "deno browser child span", - "type": "task" + "name": "deno browser child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-parent", @@ -389,18 +455,22 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 4, "name": "deno browser nested parent span", "type": "task" }, - "span_id": "" + "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-child", @@ -412,18 +482,24 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 5, - "name": "deno browser nested child span", - "type": "task" + "name": "deno browser nested child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-grandchild", @@ -434,19 +510,28 @@ "end": 0, "start": 0 }, + "output": { + "depth": 3 + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 6, - "name": "deno browser nested grandchild span", - "type": "task" + "name": "deno browser nested grandchild span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "current-span", @@ -457,14 +542,17 @@ "end": 0, "start": 0 }, + "output": { + "observedSpanId": "" + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 7, "name": "deno browser current span", "type": "task" }, - "span_id": "" + "span_id": "" } ] } diff --git a/e2e/scenarios/deno-browser/__snapshots__/span-tree.json b/e2e/scenarios/deno-browser/__snapshots__/span-tree.json index 9461d0edb..d5ffa8e80 100644 --- a/e2e/scenarios/deno-browser/__snapshots__/span-tree.json +++ b/e2e/scenarios/deno-browser/__snapshots__/span-tree.json @@ -39,7 +39,25 @@ { "name": "deno browser parent span", "type": "task", - "children": [], + "children": [ + { + "name": "deno browser child span", + "children": [], + "input": { + "step": "child", + "testRunId": "" + }, + "output": { + "ok": true, + "phase": "child" + }, + "metadata": { + "case": "child-span", + "scenario": "deno-browser", + "testRunId": "" + } + } + ], "input": { "phase": "parent", "testRunId": "" @@ -54,58 +72,46 @@ "testRunId": "" } }, - { - "name": "deno browser child span", - "type": "task", - "children": [], - "input": { - "step": "child", - "testRunId": "" - }, - "output": { - "ok": true, - "phase": "child" - }, - "metadata": { - "case": "child-span", - "scenario": "deno-browser", - "testRunId": "" - } - }, { "name": "deno browser nested parent span", "type": "task", - "children": [], + "children": [ + { + "name": "deno browser nested child span", + "children": [ + { + "name": "deno browser nested grandchild span", + "children": [], + "output": { + "depth": 3 + }, + "metadata": { + "case": "nested-grandchild", + "scenario": "deno-browser", + "testRunId": "" + } + } + ], + "metadata": { + "case": "nested-child", + "scenario": "deno-browser", + "testRunId": "" + } + } + ], "metadata": { "case": "nested-parent", "scenario": "deno-browser", "testRunId": "" } }, - { - "name": "deno browser nested child span", - "type": "task", - "children": [], - "metadata": { - "case": "nested-child", - "scenario": "deno-browser", - "testRunId": "" - } - }, - { - "name": "deno browser nested grandchild span", - "type": "task", - "children": [], - "metadata": { - "case": "nested-grandchild", - "scenario": "deno-browser", - "testRunId": "" - } - }, { "name": "deno browser current span", "type": "task", "children": [], + "output": { + "observedSpanId": "" + }, "metadata": { "case": "current-span", "scenario": "deno-browser", diff --git a/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt b/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt index 9af1c12e1..3f459283e 100644 --- a/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt +++ b/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt @@ -41,39 +41,45 @@ span_tree: │ "scenario": "deno-browser", │ "testRunId": "" │ } -├── deno browser child span [task] -│ input: { -│ "step": "child", -│ "testRunId": "" -│ } -│ output: { -│ "ok": true, -│ "phase": "child" -│ } -│ metadata: { -│ "case": "child-span", -│ "scenario": "deno-browser", -│ "testRunId": "" -│ } +│ └── deno browser child span +│ input: { +│ "step": "child", +│ "testRunId": "" +│ } +│ output: { +│ "ok": true, +│ "phase": "child" +│ } +│ metadata: { +│ "case": "child-span", +│ "scenario": "deno-browser", +│ "testRunId": "" +│ } ├── deno browser nested parent span [task] │ metadata: { │ "case": "nested-parent", │ "scenario": "deno-browser", │ "testRunId": "" │ } -├── deno browser nested child span [task] -│ metadata: { -│ "case": "nested-child", -│ "scenario": "deno-browser", -│ "testRunId": "" -│ } -├── deno browser nested grandchild span [task] -│ metadata: { -│ "case": "nested-grandchild", -│ "scenario": "deno-browser", -│ "testRunId": "" -│ } +│ └── deno browser nested child span +│ metadata: { +│ "case": "nested-child", +│ "scenario": "deno-browser", +│ "testRunId": "" +│ } +│ └── deno browser nested grandchild span +│ output: { +│ "depth": 3 +│ } +│ metadata: { +│ "case": "nested-grandchild", +│ "scenario": "deno-browser", +│ "testRunId": "" +│ } └── deno browser current span [task] + output: { + "observedSpanId": "" + } metadata: { "case": "current-span", "scenario": "deno-browser", diff --git a/e2e/scenarios/deno-browser/runner.case.ts b/e2e/scenarios/deno-browser/runner.case.ts index 88197b73b..823b53dc6 100644 --- a/e2e/scenarios/deno-browser/runner.case.ts +++ b/e2e/scenarios/deno-browser/runner.case.ts @@ -1,6 +1,5 @@ -import * as braintrust from "braintrust/browser"; +import * as braintrust from "@braintrust/browser"; import { - expectBuildType, expectEvalWorks, expectMustacheTemplate, expectNamedExports, @@ -27,10 +26,8 @@ Deno.test("deno-browser exposes the expected browser runtime surface", () => { "initLogger", "currentSpan", "startSpan", - "log", "flush", "initDataset", - "initExperiment", "loadPrompt", "Prompt", "Eval", @@ -40,14 +37,10 @@ Deno.test("deno-browser exposes the expected browser runtime surface", () => { "Attachment", "invoke", "initFunction", - "Project", - "PromptBuilder", - "IDGenerator", + "projects", "BraintrustState", "login", - "_exportsForTestingOnly", ]); - expectBuildType(braintrust, "browser"); expectMustacheTemplate(braintrust); expectNunjucksTemplateUnavailable(braintrust); }); diff --git a/e2e/scenarios/deno-browser/scenario.test.ts b/e2e/scenarios/deno-browser/scenario.test.ts index 46fe46f7d..eb066f2d1 100644 --- a/e2e/scenarios/deno-browser/scenario.test.ts +++ b/e2e/scenarios/deno-browser/scenario.test.ts @@ -128,11 +128,15 @@ test( phase: "child", ok: true, }); - expect(childSpan?.span.parentIds).toEqual([]); - expect(nestedChild?.span.parentIds).toEqual([]); - expect(nestedGrandchild?.span.parentIds).toEqual([]); - expect(nestedGrandchild?.output).toBeUndefined(); - expect(currentSpan?.output).toBeUndefined(); + expect(childSpan?.span.parentIds).toEqual([parentSpan?.span.id]); + expect(nestedChild?.span.parentIds).toEqual([nestedParent?.span.id]); + expect(nestedGrandchild?.span.parentIds).toEqual([ + nestedChild?.span.id, + ]); + expect(nestedGrandchild?.output).toEqual({ depth: 3 }); + expect(currentSpan?.output).toMatchObject({ + observedSpanId: currentSpan?.span.id, + }); const requests = requestsAfter( cursor, diff --git a/e2e/scenarios/deno-node/runner.case.ts b/e2e/scenarios/deno-node/runner.case.ts index b8f88b2db..2f21d1d91 100644 --- a/e2e/scenarios/deno-node/runner.case.ts +++ b/e2e/scenarios/deno-node/runner.case.ts @@ -1,6 +1,5 @@ import * as braintrust from "braintrust"; import { - expectBuildType, expectEvalWorks, expectMustacheTemplate, expectNamedExports, @@ -27,10 +26,8 @@ Deno.test("deno-node exposes the expected node runtime surface", () => { "initLogger", "currentSpan", "startSpan", - "log", "flush", "initDataset", - "initExperiment", "loadPrompt", "Prompt", "Eval", @@ -40,14 +37,10 @@ Deno.test("deno-node exposes the expected node runtime surface", () => { "Attachment", "invoke", "initFunction", - "Project", - "PromptBuilder", - "IDGenerator", + "projects", "BraintrustState", "login", - "_exportsForTestingOnly", ]); - expectBuildType(braintrust, "node"); expectMustacheTemplate(braintrust); expectNunjucksTemplateUnavailable(braintrust); }); diff --git a/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts index 9ba5484f2..b16977e74 100644 --- a/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts +++ b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts @@ -1,4 +1,5 @@ -import { braintrustEveInstrumentation, initLogger } from "braintrust"; +import { initLogger } from "braintrust"; +import { braintrustEveInstrumentation } from "braintrust/instrumentation"; import { defineInstrumentation } from "eve/instrumentation"; export default defineInstrumentation( diff --git a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts index 9ba5484f2..b16977e74 100644 --- a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts +++ b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts @@ -1,4 +1,5 @@ -import { braintrustEveInstrumentation, initLogger } from "braintrust"; +import { initLogger } from "braintrust"; +import { braintrustEveInstrumentation } from "braintrust/instrumentation"; import { defineInstrumentation } from "eve/instrumentation"; export default defineInstrumentation( diff --git a/e2e/scenarios/eve-instrumentation/scenario.ts b/e2e/scenarios/eve-instrumentation/scenario.ts index ca1c92027..446677607 100644 --- a/e2e/scenarios/eve-instrumentation/scenario.ts +++ b/e2e/scenarios/eve-instrumentation/scenario.ts @@ -80,7 +80,7 @@ async function main() { ); // Eve emits session.waiting just before its durable session snapshot is // visible to the continuation route. - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 3000)); const followUp = await fetch( `${baseUrl}/eve/v1/session/${body.sessionId}`, { diff --git a/e2e/scenarios/flue-instrumentation/.flue/app.mjs b/e2e/scenarios/flue-instrumentation/.flue/app.mjs index 5d3ce1360..7e22bc269 100644 --- a/e2e/scenarios/flue-instrumentation/.flue/app.mjs +++ b/e2e/scenarios/flue-instrumentation/.flue/app.mjs @@ -1,4 +1,5 @@ -import { braintrustFlueInstrumentation, flush, initLogger } from "braintrust"; +import { flush, initLogger } from "braintrust"; +import { braintrustFlueInstrumentation } from "braintrust/instrumentation"; import { Hono } from "hono"; const runtimePackageName = diff --git a/e2e/scenarios/flue-instrumentation/scenario.v2.ts b/e2e/scenarios/flue-instrumentation/scenario.v2.ts index 567d47321..8d9c41a06 100644 --- a/e2e/scenarios/flue-instrumentation/scenario.v2.ts +++ b/e2e/scenarios/flue-instrumentation/scenario.v2.ts @@ -1,10 +1,6 @@ import { openaiProvider } from "pi-ai-v2/providers/openai"; -import { - braintrustFlueInstrumentation, - flush, - initLogger, - traced, -} from "braintrust"; +import { flush, initLogger, traced } from "braintrust"; +import { braintrustFlueInstrumentation } from "braintrust/instrumentation"; import * as v from "valibot"; const SCENARIO_NAME = "flue-instrumentation"; diff --git a/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts b/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts index f3a2d4bb1..f74828b5e 100644 --- a/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts +++ b/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts @@ -1,7 +1,7 @@ import { context as otelContext, trace } from "@opentelemetry/api"; import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; import { BraintrustSpanProcessor, setupOtelCompat } from "@braintrust/otel"; -import { getContextManager, initLogger } from "braintrust"; +import { initLogger, withCurrent } from "braintrust"; import { createTracerProvider, getTestRunId, @@ -39,9 +39,7 @@ async function main() { }, }, }); - const contextManagerFacade = getContextManager(); - - await contextManagerFacade.runInContext(btRoot, async () => { + await withCurrent(btRoot, async () => { await tracer.startActiveSpan("otel-middle", async (otelSpan) => { const btChild = logger.startSpan({ name: "bt-child-under-otel", diff --git a/e2e/scenarios/test-framework-evals-jest/runner.case.cjs b/e2e/scenarios/test-framework-evals-jest/runner.case.cjs index cffa7756d..b2886017b 100644 --- a/e2e/scenarios/test-framework-evals-jest/runner.case.cjs +++ b/e2e/scenarios/test-framework-evals-jest/runner.case.cjs @@ -2,7 +2,6 @@ const { Eval, JSONAttachment, Prompt, - _exportsForTestingOnly, currentSpan, initLogger, startSpan, @@ -40,10 +39,8 @@ test("jest exposes the core braintrust runtime surface in CommonJS", () => { "initLogger", "currentSpan", "startSpan", - "log", "flush", "initDataset", - "initExperiment", "loadPrompt", "Prompt", "Eval", @@ -53,20 +50,12 @@ test("jest exposes the core braintrust runtime surface in CommonJS", () => { "Attachment", "invoke", "initFunction", - "Project", - "PromptBuilder", - "IDGenerator", + "projects", "BraintrustState", "login", - "_exportsForTestingOnly", ]); }); -test("jest resolves the node build in CommonJS mode", () => { - expect(_exportsForTestingOnly).toBeDefined(); - expect(_exportsForTestingOnly.isomorph.buildType).toBe("node"); -}); - test("jest renders mustache prompts and fails clearly for nunjucks without the extra package", () => { const mustachePrompt = new Prompt( { diff --git a/e2e/scenarios/trace-context-and-continuation/scenario.ts b/e2e/scenarios/trace-context-and-continuation/scenario.ts index c0bb88ec3..bcf1371ba 100644 --- a/e2e/scenarios/trace-context-and-continuation/scenario.ts +++ b/e2e/scenarios/trace-context-and-continuation/scenario.ts @@ -1,5 +1,6 @@ import { flush, + extractTraceContextFromHeaders, initLogger, startSpan, traced, @@ -28,7 +29,7 @@ async function main() { }, }, }); - const exportedRoot = await rootSpan.export(); + const propagatedRoot = extractTraceContextFromHeaders(rootSpan.inject()); await withCurrent(rootSpan, async () => { const currentChild = startSpan({ @@ -50,7 +51,7 @@ async function main() { rootSpan.end(); - await withParent(exportedRoot, async () => { + await withParent(propagatedRoot, async () => { await traced( (span) => { span.log({ diff --git a/integrations/browser-js/src/browser-config.ts b/integrations/browser-js/src/browser-config.ts index ddf239c33..ae9c788a6 100644 --- a/integrations/browser-js/src/browser-config.ts +++ b/integrations/browser-js/src/browser-config.ts @@ -1,41 +1,35 @@ -import { _internalIso as iso, _internalSetInitialState } from "braintrust"; +import { + configureContextManager, + ContextManager, + type ContextParentSpanIds, + type CurrentSpanStore, + type Span, +} from "braintrust"; import { AsyncLocalStorage as BrowserAsyncLocalStorage } from "als-browser"; -export function configureBrowser() { - // Set build type indicator - iso.buildType = "browser-js" as "browser"; +class BrowserContextManager extends ContextManager { + private readonly currentSpan = new BrowserAsyncLocalStorage(); - iso.newAsyncLocalStorage = () => new BrowserAsyncLocalStorage(); + getParentSpanIds(): ContextParentSpanIds | undefined { + const span = this.currentSpan.getStore(); + return span + ? { rootSpanId: span.rootSpanId, spanParents: [span.spanId] } + : undefined; + } - iso.getEnv = (name: string) => { - if (typeof process === "undefined" || typeof process.env === "undefined") { - return undefined; - } - return process.env[name]; - }; + runInContext(span: Span, callback: () => R): R { + return this.currentSpan.run(span, callback); + } - // noop implementations for git config - iso.getRepoInfo = async () => ({ - commit: null, - branch: null, - tag: null, - dirty: false, - }); - iso.getCallerLocation = () => undefined; + getCurrentSpan(): Span | undefined { + return this.currentSpan.getStore(); + } - // Implement browser-compatible hash function using a simple hash algorithm - iso.hash = (data: string): string => { - // Simple hash function for browser compatibility - let hash = 0; - for (let i = 0; i < data.length; i++) { - const char = data.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer - } - // Convert to hex string - const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); - return hashHex.repeat(8).substring(0, 64); // Make it look like a SHA-256 hash length - }; + getCurrentSpanStore(): CurrentSpanStore { + return this.currentSpan; + } +} - _internalSetInitialState(); +export function configureBrowser(): void { + configureContextManager(BrowserContextManager); } diff --git a/integrations/langchain-js/src/test/utils.ts b/integrations/langchain-js/src/test/utils.ts index 823dc5b71..52761197c 100644 --- a/integrations/langchain-js/src/test/utils.ts +++ b/integrations/langchain-js/src/test/utils.ts @@ -1,4 +1,4 @@ -import { mergeDicts } from "braintrust/util"; +import { mergeDicts } from "../../../../js/util/object_util"; import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; import { LogsRequest } from "./types"; diff --git a/integrations/openai-agents-js/src/index.ts b/integrations/openai-agents-js/src/index.ts index beafc3fc0..233bfa9bb 100644 --- a/integrations/openai-agents-js/src/index.ts +++ b/integrations/openai-agents-js/src/index.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { SpanTypeAttribute } from "braintrust/util"; import { Span as BraintrustSpan, startSpan, @@ -7,6 +6,7 @@ import { currentSpan, NOOP_SPAN, Attachment, + type StartSpanArgs, } from "braintrust"; import { SpanType, @@ -29,7 +29,9 @@ import { isSpeechGroupSpanData, } from "./types"; -function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { +function spanTypeFromAgents( + span: AgentsSpan, +): NonNullable { const spanType = span.spanData.type; if ( @@ -38,7 +40,7 @@ function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { spanType === SpanType.CUSTOM || spanType === SpanType.SPEECH_GROUP ) { - return SpanTypeAttribute.TASK; + return "task"; } if ( @@ -46,7 +48,7 @@ function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { spanType === SpanType.GUARDRAIL || spanType === SpanType.MCP_TOOLS ) { - return SpanTypeAttribute.TOOL; + return "tool"; } if ( @@ -55,10 +57,10 @@ function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { spanType === SpanType.TRANSCRIPTION || spanType === SpanType.SPEECH ) { - return SpanTypeAttribute.LLM; + return "llm"; } - return SpanTypeAttribute.TASK; + return "task"; } function spanNameFromAgents(span: AgentsSpan): string { @@ -282,18 +284,18 @@ export class OpenAIAgentsTraceProcessor { // Create as child of current span span = current.startSpan({ name: trace.name, - type: SpanTypeAttribute.TASK, + type: "task", }); } else { // No parent span available, create as root span = this.logger ? this.logger.startSpan({ name: trace.name, - type: SpanTypeAttribute.TASK, + type: "task", }) : startSpan({ name: trace.name, - type: SpanTypeAttribute.TASK, + type: "task", }); } diff --git a/integrations/openai-agents-js/src/openai-agents-integration.test.ts b/integrations/openai-agents-js/src/openai-agents-integration.test.ts index 71a6e5fd5..e6f59c0cd 100644 --- a/integrations/openai-agents-js/src/openai-agents-integration.test.ts +++ b/integrations/openai-agents-js/src/openai-agents-integration.test.ts @@ -11,14 +11,16 @@ import { z } from "zod/v3"; // Import necessary types and functions from braintrust import { - _exportsForTestingOnly, initLogger, Logger, - TestBackgroundLogger, Span as BraintrustSpan, wrapTraced, currentSpan, } from "braintrust"; +import { + _exportsForTestingOnly, + type TestBackgroundLogger, +} from "@braintrust-test/logger"; // Test helper functions for backward compatibility function getSpansMap( diff --git a/integrations/openai-agents-js/src/test-logger.d.ts b/integrations/openai-agents-js/src/test-logger.d.ts new file mode 100644 index 000000000..1cc19256d --- /dev/null +++ b/integrations/openai-agents-js/src/test-logger.d.ts @@ -0,0 +1,12 @@ +declare module "@braintrust-test/logger" { + export interface TestBackgroundLogger { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + drain(): Promise; + } + + export const _exportsForTestingOnly: { + simulateLoginForTests(): Promise; + useTestBackgroundLogger(): TestBackgroundLogger; + clearTestBackgroundLogger(): void; + }; +} diff --git a/integrations/openai-agents-js/tsconfig.json b/integrations/openai-agents-js/tsconfig.json index ea6ad1861..5fd07c79d 100644 --- a/integrations/openai-agents-js/tsconfig.json +++ b/integrations/openai-agents-js/tsconfig.json @@ -10,6 +10,6 @@ "esModuleInterop": true, "skipLibCheck": true }, - "include": ["."], + "include": ["src"], "exclude": ["node_modules/**", "**/dist/**"] } diff --git a/integrations/openai-agents-js/vitest.config.ts b/integrations/openai-agents-js/vitest.config.ts index 9b3c6bb43..1c14e5ef6 100644 --- a/integrations/openai-agents-js/vitest.config.ts +++ b/integrations/openai-agents-js/vitest.config.ts @@ -1,6 +1,17 @@ import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; export default defineConfig({ + resolve: { + alias: { + "@braintrust-test/logger": fileURLToPath( + new URL("../../js/src/logger.ts", import.meta.url), + ), + braintrust: fileURLToPath( + new URL("../../js/src/node/index.ts", import.meta.url), + ), + }, + }, test: { reporters: ["default"], include: ["tests/**/*.test.ts", "src/**/*.test.ts"], diff --git a/integrations/otel-js/README.md b/integrations/otel-js/README.md index f5adf45c2..e7afa2da5 100644 --- a/integrations/otel-js/README.md +++ b/integrations/otel-js/README.md @@ -118,28 +118,23 @@ const logger = initLogger({ projectName: "my_project" }); **What it does**: - Sets Braintrust to use OpenTelemetry's context manager for parent-child span relationships -- Configures Braintrust to use OpenTelemetry-compatible span and trace IDs - Enables seamless mixing of Braintrust and OpenTelemetry spans in the same trace ## Distributed Tracing Helpers These utilities enable distributed tracing across services that use different tracing systems (Braintrust and OpenTelemetry). -### `contextFromSpanExport(exportStr: string)` +### `contextFromSpan(span: Span)` -Creates an OpenTelemetry context from a Braintrust span export string. +Creates an OpenTelemetry context from a Braintrust span using W3C Trace Context. -**When to use it**: When Service A uses Braintrust and sends a span export to Service B that uses OpenTelemetry. This allows Service B to create spans as children of Service A's span. +**When to use it**: When Braintrust and OpenTelemetry code run in the same process and an OpenTelemetry operation should be a child of a Braintrust span. Across services, propagate the headers returned by `span.inject()` instead. ```typescript -import { contextFromSpanExport } from "@braintrust/otel"; +import { contextFromSpan } from "@braintrust/otel"; import { context } from "@opentelemetry/api"; -// Service A (Braintrust) exports span -const exportedSpan = await spanA.export(); - -// Service B (OpenTelemetry) imports context -const ctx = contextFromSpanExport(exportedSpan); +const ctx = contextFromSpan(spanA); await context.with(ctx, async () => { // OpenTelemetry spans created here will be children of Service A's span await tracer.startActiveSpan("service_b_operation", async (span) => { diff --git a/integrations/otel-js/src/context.ts b/integrations/otel-js/src/context.ts index 6db697c67..b61416e41 100644 --- a/integrations/otel-js/src/context.ts +++ b/integrations/otel-js/src/context.ts @@ -1,11 +1,10 @@ import { ContextManager, - BRAINTRUST_CURRENT_SPAN_STORE, - _internalIso as iso, type ContextParentSpanIds, type CurrentSpanStore, type Span, } from "braintrust"; +import { AsyncLocalStorage } from "node:async_hooks"; import { trace as otelTrace, context as otelContext } from "@opentelemetry/api"; import { getOtelParentFromSpan } from "./otel"; @@ -91,29 +90,11 @@ export class OtelContextManager extends ContextManager { return (otelContext as any)._getContextManager?.()._asyncLocalStorage; } - constructor() { - super(); - // Expose whichever ALS is in use via BRAINTRUST_CURRENT_SPAN_STORE so that - // TracingChannel's bindStore can propagate span context. We prefer OTEL's own - // ALS (AsyncLocalStorageContextManager._asyncLocalStorage) so that spans - // stored by runStores are visible to OTEL's context APIs. If the active OTEL - // context manager doesn't expose an ALS (e.g. AsyncHooksContextManager), we - // fall back to our own IsoAsyncLocalStorage and behave like the default - // BraintrustContextManager for TracingChannel binding. - // - // A lazy getter is required because the global OTEL context manager may not be - // registered until after this instance is constructed. - const self = this; - Object.defineProperty(this, BRAINTRUST_CURRENT_SPAN_STORE, { - get(): CurrentSpanStore { - const otelAls = self._getOtelAls(); - if (otelAls) return otelAls; - if (!self._ownAls) self._ownAls = iso.newAsyncLocalStorage(); - return self._ownAls; - }, - configurable: true, - enumerable: false, - }); + getCurrentSpanStore(): CurrentSpanStore { + const otelAls = this._getOtelAls(); + if (otelAls) return otelAls; + if (!this._ownAls) this._ownAls = new AsyncLocalStorage(); + return this._ownAls; } wrapSpanForStore(span: Span): unknown { diff --git a/integrations/otel-js/src/exporter.test.ts b/integrations/otel-js/src/exporter.test.ts index 94168963a..b0391222d 100644 --- a/integrations/otel-js/src/exporter.test.ts +++ b/integrations/otel-js/src/exporter.test.ts @@ -22,7 +22,7 @@ import { } from "@opentelemetry/sdk-trace-base"; import { BraintrustSpanProcessor } from "./otel"; import { createTracerProvider } from "../tests/utils"; -import { _exportsForTestingOnly } from "braintrust"; +import { _exportsForTestingOnly } from "../../../js/src/logger"; type OtlpTraceRequest = { resourceSpans?: Array<{ diff --git a/integrations/otel-js/src/id-gen.test.ts b/integrations/otel-js/src/id-gen.test.ts index b38e0afbc..19fcb6f93 100644 --- a/integrations/otel-js/src/id-gen.test.ts +++ b/integrations/otel-js/src/id-gen.test.ts @@ -1,6 +1,9 @@ import { expect, test, describe, beforeEach, afterEach } from "vitest"; -import { UUIDGenerator, getIdGenerator } from "braintrust"; -import { OTELIDGenerator } from "./otel"; +import { + OTELIDGenerator, + UUIDGenerator, + getIdGenerator, +} from "../../../js/src/id-gen"; import { setupOtelCompat, resetOtelCompat } from "."; describe("ID Generation", () => { diff --git a/integrations/otel-js/src/index.ts b/integrations/otel-js/src/index.ts index 9555cd32c..41f2b20cb 100644 --- a/integrations/otel-js/src/index.ts +++ b/integrations/otel-js/src/index.ts @@ -1,48 +1,20 @@ import { OtelContextManager } from "./context"; -import { _internalGetGlobalState } from "braintrust"; +import { configureContextManager } from "braintrust"; export { - contextFromSpanExport, + contextFromSpan, addSpanParentToBaggage, addParentToBaggage, parentFromHeaders, isRootSpan, } from "./otel"; -import { OTELIDGenerator } from "./otel"; -import { SpanComponentsV4 } from "braintrust/util"; - export { BraintrustSpanProcessor, BraintrustExporter } from "./otel"; -function resetBraintrustCompatCaches() { - const state = _internalGetGlobalState(); - if (!state) { - return; - } - - // Node/browser package initialization can cache native context and ID state - // before setupOtelCompat() runs. Reset both so subsequent lookups honor the - // compat-mode globals we are about to install. - (state as unknown as { _contextManager: unknown })._contextManager = null; - (state as unknown as { _idGenerator: unknown })._idGenerator = null; -} - export const setupOtelCompat = () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_CONTEXT_MANAGER = OtelContextManager; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_ID_GENERATOR = OTELIDGenerator; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_SPAN_COMPONENT = SpanComponentsV4; - resetBraintrustCompatCaches(); + configureContextManager(OtelContextManager); }; export const resetOtelCompat = () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_CONTEXT_MANAGER = undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_ID_GENERATOR = undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_SPAN_COMPONENT = undefined; - resetBraintrustCompatCaches(); + configureContextManager(undefined); }; diff --git a/integrations/otel-js/src/otel-compat.test.ts b/integrations/otel-js/src/otel-compat.test.ts index d06d9f843..35db904c6 100644 --- a/integrations/otel-js/src/otel-compat.test.ts +++ b/integrations/otel-js/src/otel-compat.test.ts @@ -6,14 +6,13 @@ */ import { beforeEach, afterEach, describe, expect, test } from "vitest"; +import { initLogger, currentSpan, Eval } from "braintrust"; import { - initLogger, - currentSpan, - getContextManager, - BRAINTRUST_CURRENT_SPAN_STORE, _exportsForTestingOnly, - Eval, -} from "braintrust"; + _internalStartSpan, + _internalWithParent, + getContextManager, +} from "../../../js/src/logger"; import { BasicTracerProvider, InMemorySpanExporter, @@ -264,15 +263,13 @@ describe("OTEL compatibility mode", () => { expect(cm.getCurrentSpan).toBeDefined(); }); - test("uses OtelContextManager when OTEL enabled", async () => { + test("uses OtelContextManager when OTEL enabled", () => { // Test that when OTEL is available and env var is set, we get OtelContextManager // Note: In test environment, OTEL packages may not be available even though // we checked OTEL_AVAILABLE. If the require fails in getContextManager, // it falls back to BraintrustContextManager, which is correct behavior. - // Clear module cache and re-import to get fresh context manager - const loggerModule = await import("braintrust?t=" + Date.now()); - const cm = loggerModule.getContextManager(); + const cm = getContextManager(); // If OTEL is truly available, we should get OtelContextManager // Otherwise, fallback to BraintrustContextManager is acceptable @@ -652,7 +649,7 @@ describe("OTEL compatibility mode", () => { parentSpan.end(); - const childSpan = logger.startSpan({ + const childSpan = _internalStartSpan({ name: "child-span-v4", parent: exported, }); @@ -678,11 +675,9 @@ describe("OTEL compatibility mode", () => { parentSpan.end(); // Use withParent helper with logger.startSpan to properly handle the exported parent - const { withParent } = await import("braintrust"); - - withParent(exported, () => { + _internalWithParent(exported, () => { // Pass parent explicitly in addition to withParent context - const childSpan = logger.startSpan({ + const childSpan = _internalStartSpan({ name: "child-span-v4-with-parent", parent: exported, }); @@ -720,12 +715,11 @@ describe("OtelContextManager TracingChannel integration", () => { resetOtelCompat(); }); - test("OtelContextManager exposes OTEL ALS via BRAINTRUST_CURRENT_SPAN_STORE", () => { + test("OtelContextManager exposes the OTEL ALS", () => { const cm = getContextManager(); expect(cm.constructor.name).toBe("OtelContextManager"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; expect(store).toBeDefined(); expect(typeof store.run).toBe("function"); expect(typeof store.getStore).toBe("function"); @@ -756,8 +750,7 @@ describe("OtelContextManager TracingChannel integration", () => { test("store.run() with wrapSpanForStore output propagates span to currentSpan()", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; expect(store).toBeDefined(); const mockSpan = { @@ -778,8 +771,7 @@ describe("OtelContextManager TracingChannel integration", () => { test("nested store.run() calls maintain correct span chain", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; const parentSpan = { spanId: "1111111111111111", @@ -863,10 +855,9 @@ describe("OtelContextManager fallback ALS (AsyncHooksContextManager)", () => { resetOtelCompat(); }); - test("exposes fallback ALS via BRAINTRUST_CURRENT_SPAN_STORE when OTEL ALS is unavailable", () => { + test("exposes fallback ALS when OTEL ALS is unavailable", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; expect(store).toBeDefined(); expect(typeof store.run).toBe("function"); expect(typeof store.getStore).toBe("function"); @@ -889,8 +880,7 @@ describe("OtelContextManager fallback ALS (AsyncHooksContextManager)", () => { test("store.run() with span in fallback mode makes getCurrentSpan() work", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; const mockSpan = { spanId: "fedcba0987654321", @@ -938,29 +928,16 @@ describe("Distributed Tracing (BT → OTEL)", () => { resetOtelCompat(); }); - test("otelContextFromSpanExport parses BT span and creates OTEL context", async () => { - const { contextFromSpanExport: otelContextFromSpanExport } = - await import("./"); - const { SpanComponentsV4 } = await import("braintrust/util"); - const { SpanObjectTypeV3 } = await import("braintrust/util"); - - // Create a sample span export string + test("contextFromSpan creates an OTEL context", async () => { + const { contextFromSpan } = await import("./"); const rootSpanId = "a1b2c3d4e5f6789012345678abcdef01"; // 32 hex chars (16 bytes) const spanId = "a1b2c3d4e5f67890"; // 16 hex chars (8 bytes) - const objectId = "proj-123"; - - const components = new SpanComponentsV4({ - object_type: SpanObjectTypeV3.PROJECT_LOGS, - object_id: objectId, - row_id: "row-123", - span_id: spanId, - root_span_id: rootSpanId, - propagated_event: undefined, - }); - - const exportStr = components.toStr(); - - const ctx = otelContextFromSpanExport(exportStr); + const ctx = contextFromSpan({ + inject: () => ({ + traceparent: `00-${rootSpanId}-${spanId}-01`, + baggage: "braintrust.parent=project_id%3Aproj-123", + }), + } as any); // Verify that a valid context was created expect(ctx).toBeDefined(); @@ -985,8 +962,7 @@ describe("Distributed Tracing (BT → OTEL)", () => { test("BT span in Service A can be parent of OTEL span in Service B", async () => { const { tracer, exporter, processor } = setupOtelFixture("service-a-project"); - const { contextFromSpanExport: otelContextFromSpanExport } = - await import("./"); + const { contextFromSpan } = await import("./"); const projectName = "service-a-project"; const logger = initLogger({ projectName }); @@ -994,25 +970,23 @@ describe("Distributed Tracing (BT → OTEL)", () => { // ===== Service A: Create BT span and export ===== let serviceATraceId: string | undefined; let serviceASpanId: string | undefined; - let exportedContext: string | undefined; + let sourceSpan: import("braintrust").Span | undefined; await logger.traced( async (serviceASpan) => { serviceATraceId = serviceASpan.rootSpanId; serviceASpanId = serviceASpan.spanId; - - // Export context for sending to Service B (e.g., via HTTP header) - exportedContext = await serviceASpan.export(); + sourceSpan = serviceASpan; }, { name: "service_a_span" }, ); - expect(exportedContext).toBeDefined(); + expect(sourceSpan).toBeDefined(); expect(serviceATraceId).toBeDefined(); expect(serviceASpanId).toBeDefined(); // ===== Service B: Import context and create OTEL child span ===== - const ctx = otelContextFromSpanExport(exportedContext!); + const ctx = contextFromSpan(sourceSpan!); // Use context.with() to run code in the imported context await context.with(ctx, async () => { diff --git a/integrations/otel-js/src/otel.test.ts b/integrations/otel-js/src/otel.test.ts index 7522450da..e34e2a36b 100644 --- a/integrations/otel-js/src/otel.test.ts +++ b/integrations/otel-js/src/otel.test.ts @@ -26,13 +26,14 @@ import { parentFromHeaders, isRootSpan, } from "./otel"; -import { _exportsForTestingOnly, initLogger } from "braintrust"; +import { initLogger } from "braintrust"; +import { SpanComponentsV3, SpanComponentsV4 } from "../../../js/util"; +import { _exportsForTestingOnly } from "../../../js/src/logger"; import { base64ToUint8Array, getExportVersion, createTracerProvider, } from "../tests/utils"; -import { SpanComponentsV3, SpanComponentsV4 } from "braintrust/util"; import { setupOtelCompat, resetOtelCompat } from "."; const packageJson = JSON.parse( @@ -61,6 +62,13 @@ async function withEmptyBraintrustEnvFile( return withBraintrustEnvFile("", fn); } +function restoreEnvironment(snapshot: NodeJS.ProcessEnv): void { + for (const name of Object.keys(process.env)) { + if (!(name in snapshot)) delete process.env[name]; + } + Object.assign(process.env, snapshot); +} + describe("AISpanProcessor", () => { let memoryExporter: InMemorySpanExporter; let provider: BasicTracerProvider; @@ -605,7 +613,7 @@ describe("BraintrustSpanProcessor", () => { }); afterEach(() => { - process.env = originalEnv; + restoreEnvironment(originalEnv); vi.restoreAllMocks(); _exportsForTestingOnly.clearTestBackgroundLogger(); _exportsForTestingOnly.simulateLogoutForTests(); @@ -1008,7 +1016,7 @@ describe("BraintrustExporter", () => { }); afterEach(() => { - process.env = originalEnv; + restoreEnvironment(originalEnv); vi.restoreAllMocks(); _exportsForTestingOnly.clearTestBackgroundLogger(); _exportsForTestingOnly.simulateLogoutForTests(); @@ -1392,180 +1400,35 @@ describe("otel namespace helpers", () => { }); describe("parentFromHeaders", () => { - describe("valid inputs", () => { - it("should extract parent from headers with valid traceparent and braintrust.parent baggage", () => { - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeDefined(); - // Parent string is base64-encoded SpanComponentsV4 - expect(typeof parent).toBe("string"); - expect(parent!.length).toBeGreaterThan(0); - }); - - it("should extract parent with project_id", () => { - const headers = { - traceparent: - "00-abcdef12345678901234567890123456-fedcba9876543210-01", - baggage: "braintrust.parent=project_id:my-project-id", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeDefined(); - expect(typeof parent).toBe("string"); - expect(parent!.length).toBeGreaterThan(0); - }); - - it("should extract parent with experiment_id", () => { - const headers = { - traceparent: - "00-11111111111111111111111111111111-2222222222222222-01", - baggage: "braintrust.parent=experiment_id:my-experiment-id", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeDefined(); - expect(typeof parent).toBe("string"); - expect(parent!.length).toBeGreaterThan(0); - }); + it("returns the opaque W3C context", () => { + const headers = { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + tracestate: "vendor=value", + baggage: "braintrust.parent=project_name%3Atest", + }; + expect(parentFromHeaders(headers)).toEqual(headers); }); - describe("invalid inputs", () => { - it("should return undefined when traceparent is missing", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when baggage is missing", () => { - const consoleSpy = vi - .spyOn(console, "warn") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalled(); - expect(consoleSpy.mock.calls[0][0]).toContain( - "braintrust.parent not found", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when braintrust.parent is missing from baggage", () => { - const consoleSpy = vi - .spyOn(console, "warn") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - baggage: "foo=bar,baz=qux", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalled(); - expect(consoleSpy.mock.calls[0][0]).toContain( - "braintrust.parent not found", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when traceparent format is invalid", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: "invalid-traceparent", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when trace_id is all zeros", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-00000000000000000000000000000000-1234567890123456-01", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - // OTEL's extract() validates and rejects invalid trace_id - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when span_id is all zeros", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-0000000000000000-01", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - // OTEL's extract() validates and rejects invalid span_id - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when braintrust.parent format is invalid", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - baggage: "braintrust.parent=invalid", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - // Should reach our validation if span context is valid, otherwise OTEL rejects it - expect(consoleSpy).toHaveBeenCalled(); + it("keeps a valid trace context without Braintrust baggage", () => { + const headers = { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + }; + expect(parentFromHeaders(headers)).toEqual(headers); + }); - consoleSpy.mockRestore(); - }); + it.each([ + {}, + { traceparent: "invalid-traceparent" }, + { + traceparent: "00-00000000000000000000000000000000-1234567890123456-01", + }, + { + traceparent: "00-12345678901234567890123456789012-0000000000000000-01", + }, + ])("returns undefined for an invalid traceparent", (headers) => { + expect( + parentFromHeaders(headers as Record), + ).toBeUndefined(); }); }); }); @@ -1735,7 +1598,9 @@ describe("Otel Compat tests Integration", () => { expect(uuidSpan.spanId).toMatch(uuidRegex); uuidSpan.end(); - // Switch to OTEL compat (hex, wins over legacy). + // Switch to the default hex ID mode. OTEL compat only changes context + // management now; it does not override the core ID generator. + delete process.env.BRAINTRUST_LEGACY_IDS; setupOtelCompat(); _exportsForTestingOnly.resetIdGenStateForTests(); @@ -1941,26 +1806,6 @@ describe("export() format selection based on if otel is initialized", () => { expect(v4Parsed.data.object_type).toBeDefined(); }); - test("exported V4 span can be used as parent", async () => { - const testLogger = initLogger({ - projectName: "test-v4-parent", - apiKey: "test-key", - }); - - const parentSpan = testLogger.startSpan({ name: "parent-span-v4" }); - const exported = await parentSpan.export(); - parentSpan.end(); - - // Should be able to use V4 exported string as parent - const childSpan = testLogger.startSpan({ - name: "child-span-v4", - parent: exported, - }); - - expect(childSpan.rootSpanId).toBe(parentSpan.rootSpanId); - childSpan.end(); - }); - test("V4 format uses hex IDs (not UUIDs) when otel is initialized", async () => { _exportsForTestingOnly.resetIdGenStateForTests(); diff --git a/integrations/otel-js/src/otel.ts b/integrations/otel-js/src/otel.ts index dc7e23aca..bcc817017 100644 --- a/integrations/otel-js/src/otel.ts +++ b/integrations/otel-js/src/otel.ts @@ -1,14 +1,19 @@ -import { SpanComponentsV4, SpanObjectTypeV3 } from "braintrust/util"; +import { registerOtelFlush } from "braintrust/instrumentation"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { + CompositePropagator, + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from "@opentelemetry/core"; import { context, Context, diag, trace, - TraceFlags, propagation, Span, + type TextMapGetter, } from "@opentelemetry/api"; import { SpanProcessor, @@ -18,10 +23,9 @@ import { type SpanExporter, } from "@opentelemetry/sdk-trace-base"; import { - IDGenerator, - _internalIso, currentSpan, - registerOtelFlush, + extractTraceContextFromHeaders, + type PropagationContext, type Span as BraintrustSpan, } from "braintrust"; @@ -45,6 +49,14 @@ const SYSTEM_ATTRIBUTE_NAMES = new Set([ "braintrust.context_json", ]); +const braintrustW3CPropagator = new CompositePropagator({ + propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()], +}); +const traceContextGetter: TextMapGetter> = { + keys: Object.keys, + get: (carrier, key) => carrier[key], +}; + /** * Custom filter function type for span filtering. * @param span - The span to evaluate @@ -277,45 +289,100 @@ function withSpanOriginAttributes( }); } +const BRAINTRUST_ENV_SEARCH_PARENT_LIMIT = 64; + +function getEnv(name: string): string | undefined { + if (typeof process === "undefined") return undefined; + const value = process.env[name]; + if (value?.trim()) return value; + if ( + name !== "BRAINTRUST_API_KEY" && + name !== "BRAINTRUST_ENVIRONMENT_TYPE" && + name !== "BRAINTRUST_ENVIRONMENT_NAME" + ) { + return value; + } + + if (typeof process.loadEnvFile !== "function") return undefined; + + let dir: string; + try { + dir = process.cwd(); + } catch { + return undefined; + } + + for (let depth = 0; depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; depth++) { + try { + const separator = dir.includes("\\") ? "\\" : "/"; + process.loadEnvFile( + `${dir}${dir.endsWith(separator) ? "" : separator}.env.braintrust`, + ); + const fileValue = process.env[name]; + return fileValue?.trim() ? fileValue : undefined; + } catch (error) { + if ( + typeof error !== "object" || + error === null || + !("code" in error) || + error.code !== "ENOENT" + ) { + return undefined; + } + } + + const trimmed = dir.replace(/[\\/]+$/, ""); + const separatorIndex = Math.max( + trimmed.lastIndexOf("/"), + trimmed.lastIndexOf("\\"), + ); + const parent = + separatorIndex < 0 + ? dir + : separatorIndex === 0 + ? trimmed.slice(0, 1) + : trimmed.slice(0, separatorIndex); + if (parent === dir) break; + dir = parent; + } + return undefined; +} + function detectEnvironment(explicit?: { type?: string; name?: string; }): { type?: string; name?: string } | undefined { if (explicit) return explicit; - const envType = _internalIso.getEnv("BRAINTRUST_ENVIRONMENT_TYPE"); - const envName = _internalIso.getEnv("BRAINTRUST_ENVIRONMENT_NAME"); + const envType = getEnv("BRAINTRUST_ENVIRONMENT_TYPE"); + const envName = getEnv("BRAINTRUST_ENVIRONMENT_NAME"); if (envType || envName) { return { ...(envType ? { type: envType } : {}), ...(envName ? { name: envName } : {}), }; } - if (_internalIso.getEnv("GITHUB_ACTIONS")) - return { type: "ci", name: "github_actions" }; - if (_internalIso.getEnv("GITLAB_CI")) - return { type: "ci", name: "gitlab_ci" }; - if (_internalIso.getEnv("CIRCLECI")) return { type: "ci", name: "circleci" }; - if (_internalIso.getEnv("BUILDKITE")) - return { type: "ci", name: "buildkite" }; - if (_internalIso.getEnv("CI")) return { type: "ci", name: "ci" }; - if (_internalIso.getEnv("VERCEL")) return { type: "server", name: "vercel" }; - if (_internalIso.getEnv("NETLIFY")) - return { type: "server", name: "netlify" }; - const awsExecutionEnv = _internalIso.getEnv("AWS_EXECUTION_ENV"); + if (getEnv("GITHUB_ACTIONS")) return { type: "ci", name: "github_actions" }; + if (getEnv("GITLAB_CI")) return { type: "ci", name: "gitlab_ci" }; + if (getEnv("CIRCLECI")) return { type: "ci", name: "circleci" }; + if (getEnv("BUILDKITE")) return { type: "ci", name: "buildkite" }; + if (getEnv("CI")) return { type: "ci", name: "ci" }; + if (getEnv("VERCEL")) return { type: "server", name: "vercel" }; + if (getEnv("NETLIFY")) return { type: "server", name: "netlify" }; + const awsExecutionEnv = getEnv("AWS_EXECUTION_ENV"); if ( - _internalIso.getEnv("ECS_CONTAINER_METADATA_URI") || - _internalIso.getEnv("ECS_CONTAINER_METADATA_URI_V4") || + getEnv("ECS_CONTAINER_METADATA_URI") || + getEnv("ECS_CONTAINER_METADATA_URI_V4") || awsExecutionEnv?.startsWith("AWS_ECS_") ) { return { type: "server", name: "ecs" }; } if ( - _internalIso.getEnv("AWS_LAMBDA_FUNCTION_NAME") || + getEnv("AWS_LAMBDA_FUNCTION_NAME") || awsExecutionEnv?.startsWith("AWS_Lambda_") ) { return { type: "server", name: "aws_lambda" }; } - const nodeEnv = _internalIso.getEnv("NODE_ENV"); + const nodeEnv = getEnv("NODE_ENV"); if (!nodeEnv) return undefined; const normalizedNodeEnv = nodeEnv.toLowerCase(); if (normalizedNodeEnv === "production" || normalizedNodeEnv === "staging") { @@ -438,7 +505,7 @@ class LazyBraintrustOTLPTraceExporter implements SpanExporter { const apiKey = this.apiKey !== undefined ? this.apiKey - : await _internalIso.getBraintrustApiKey(); + : getEnv("BRAINTRUST_API_KEY"); if (!apiKey?.trim()) { throw new Error( "Braintrust API key is required. Set BRAINTRUST_API_KEY, define it in .env.braintrust, or pass apiKey option.", @@ -529,7 +596,7 @@ export class BraintrustSpanProcessor implements SpanProcessor { const apiKey = options.apiKey !== undefined ? options.apiKey - : _internalIso.getEnv("BRAINTRUST_API_KEY"); + : getEnv("BRAINTRUST_API_KEY"); // Get API URL from options or environment let apiUrl = @@ -668,132 +735,13 @@ export class BraintrustSpanProcessor implements SpanProcessor { } } -/** - * Create an OTEL context from a Braintrust span export string. - * - * Used for distributed tracing scenarios where a Braintrust span in one service - * needs to be the parent of an OTEL span in another service. - * - * @param exportStr - The string returned from span.export() - * @returns OTEL context that can be used when creating child spans - * - * @example - * ```typescript - * // Service A: Create BT span and export - * const span = logger.startSpan({ name: "service-a" }); - * const exportStr = await span.export(); - * // Send exportStr to Service B (e.g., via HTTP header) - * - * // Service B: Import context and create OTEL child - * import * as api from '@opentelemetry/api'; - * const ctx = contextFromSpanExport(exportStr); - * await api.context.with(ctx, async () => { - * await tracer.startActiveSpan("service-b", async (span) => { - * // This span is now a child of the Service A span - * span.end(); - * }); - * }); - * ``` - */ -export function contextFromSpanExport(exportStr: string): unknown { - // Parse the export string - const components = SpanComponentsV4.fromStr(exportStr); - - // Get trace and span IDs (already in hex format) - const traceIdHex = components.data.root_span_id; // 32 hex chars - const spanIdHex = components.data.span_id; // 16 hex chars - - if (!traceIdHex || !spanIdHex) { - throw new Error( - "Export string must contain root_span_id and span_id for distributed tracing", - ); - } - - // Create SpanContext marked as remote (critical for distributed tracing) - const spanContext = { - traceId: traceIdHex, - spanId: spanIdHex, - isRemote: true, - traceFlags: TraceFlags?.SAMPLED ?? 1, // SAMPLED flag - }; - - // Create NonRecordingSpan using wrapSpanContext and set in context - const nonRecordingSpan = trace.wrapSpanContext(spanContext); - let ctx = trace.setSpan(context.active(), nonRecordingSpan); - - // Construct braintrust.parent identifier - const braintrustParent = getBraintrustParent( - components.data.object_type, - components.data.object_id, - components.data.compute_object_metadata_args, +/** Create an OTEL context from a Braintrust span using W3C propagation. */ +export function contextFromSpan(span: BraintrustSpan): Context { + return braintrustW3CPropagator.extract( + context.active(), + span.inject(), + traceContextGetter, ); - - // Set braintrust.parent in baggage so it propagates automatically - if (braintrustParent) { - try { - // Try to set baggage if available - if (propagation) { - const baggage = - propagation.getBaggage(ctx) || propagation.createBaggage(); - ctx = propagation.setBaggage( - ctx, - baggage.setEntry("braintrust.parent", { - value: braintrustParent, - }), - ); - } - } catch (error) { - console.error( - "Failed to set braintrust.parent in baggage during context import:", - error, - ); - } - } - - return ctx; -} - -/** - * Construct a braintrust.parent identifier string from span components. - * - * @param objectType - Type of parent object (PROJECT_LOGS or EXPERIMENT) - * @param objectId - Resolved object ID (project_id or experiment_id) - * @param computeArgs - Optional dict with project_name/project_id for unresolved cases - * @returns String like "project_id:abc", "project_name:my-proj", "experiment_id:exp-123", or undefined - */ -function getBraintrustParent( - objectType: number, - objectId?: string | null, - computeArgs?: Record | null, -): string | undefined { - if (!objectType) { - return undefined; - } - - if (objectType === SpanObjectTypeV3.PROJECT_LOGS) { - if (objectId) { - return `project_id:${objectId}`; - } else if (computeArgs) { - const projectId = computeArgs["project_id"]; - const projectName = computeArgs["project_name"]; - if (typeof projectId === "string") { - return `project_id:${projectId}`; - } else if (typeof projectName === "string") { - return `project_name:${projectName}`; - } - } - } else if (objectType === SpanObjectTypeV3.EXPERIMENT) { - if (objectId) { - return `experiment_id:${objectId}`; - } else if (computeArgs) { - const experimentId = computeArgs["experiment_id"]; - if (typeof experimentId === "string") { - return `experiment_id:${experimentId}`; - } - } - } - - return undefined; } /** @@ -806,46 +754,16 @@ function getBraintrustParent( export function getOtelParentFromSpan( span: BraintrustSpan, ): string | undefined { - const parent = span.getParentInfo(); - - if (!parent || !parent.objectType || !parent.objectId) { - return undefined; - } - try { - if (parent.objectType === SpanObjectTypeV3.PROJECT_LOGS) { - const syncResult = parent.objectId.getSync(); - const id = syncResult?.value; - const args = parent.computeObjectMetadataArgs; - - if (id) { - return `project_id:${id}`; - } - - const projectName = args?.project_name; - if (typeof projectName === "string") { - return `project_name:${projectName}`; - } - } else if (parent.objectType === SpanObjectTypeV3.EXPERIMENT) { - const syncResult = parent.objectId.getSync(); - const id = syncResult?.value; - - // Debug details for experiment - // eslint-disable-next-line no-console - console.debug("[getOtelParentFromSpan] EXPERIMENT", { id }); - - if (id) { - // eslint-disable-next-line no-console - console.debug( - "[getOtelParentFromSpan] EXPERIMENT using experiment_id", - { id }, - ); - return `experiment_id:${id}`; + if (typeof span.inject !== "function") return undefined; + const baggage = span.inject().baggage; + for (const member of baggage?.split(",") ?? []) { + const [rawKey, rawValue] = member.trim().split("=", 2); + if (rawKey === "braintrust.parent" && rawValue) { + return decodeURIComponent(rawValue.split(";", 1)[0]); } } } catch (e) { - // Debug: unexpected error reading parent info - // eslint-disable-next-line no-console console.warn("[getOtelParentFromSpan] error extracting parent", e); } return undefined; @@ -1045,188 +963,9 @@ export function addSpanParentToBaggage( return addParentToBaggage(parentValue, ctx); } -/** - * Extract a Braintrust-compatible parent string from W3C Trace Context headers. - * - * This converts OTEL trace context headers (traceparent/baggage) into a format - * that can be passed as the 'parent' parameter to Braintrust's traced() method. - * - * @param headers - Dictionary with 'traceparent' and optionally 'baggage' keys - * @returns Braintrust V4 export string that can be used as parent parameter, - * or undefined if no valid span context is found or braintrust.parent is missing. - * - * @example - * ```typescript - * import { initLogger } from "braintrust"; - * import { parentFromHeaders } from "@braintrust/otel"; - * - * // Service C receives headers from Service B - * const headers = { traceparent: '00-trace_id-span_id-01', baggage: '...' }; - * const parent = parentFromHeaders(headers); - * - * const logger = initLogger({ projectName: "my-project" }); - * await logger.traced(async (span) => { - * span.log({ input: "BT span as child of OTEL parent" }); - * }, { name: "service_c", parent }); - * ``` - */ +/** Extract an opaque Braintrust parent context from W3C headers. */ export function parentFromHeaders( headers: Record, -): string | undefined { - try { - if (!propagation) { - console.error("OTEL propagation API not available"); - return undefined; - } - - // Extract context from headers using W3C Trace Context propagator - // This parses both traceparent and baggage headers - const ctx = propagation.extract(context.active(), headers); - - // Get span context directly from the extracted context - const spanContext = trace.getSpanContext(ctx); - if (!spanContext) { - console.error("parentFromHeaders: No valid span context in headers"); - return undefined; - } - - // Get trace_id and span_id from span context - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const traceIdHex = spanContext.traceId as string; - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const spanIdHex = spanContext.spanId as string; - - // Validate trace_id and span_id are not all zeros - if ( - !traceIdHex || - typeof traceIdHex !== "string" || - traceIdHex === "00000000000000000000000000000000" - ) { - console.error("parentFromHeaders: Invalid trace_id (all zeros)"); - return undefined; - } - if ( - !spanIdHex || - typeof spanIdHex !== "string" || - spanIdHex === "0000000000000000" - ) { - console.error("parentFromHeaders: Invalid span_id (all zeros)"); - return undefined; - } - - // Get braintrust.parent from baggage - const baggage = propagation.getBaggage(ctx); - const braintrustParent = baggage?.getEntry("braintrust.parent")?.value; - - if (!braintrustParent) { - console.warn( - "parentFromHeaders: braintrust.parent not found in OTEL baggage. " + - "Cannot create Braintrust parent without project information. " + - "Ensure the OTEL span sets braintrust.parent in baggage before exporting headers.", - ); - return undefined; - } - - // Parse braintrust.parent to extract object_type and object_id - let objectType: number | undefined; - let objectId: string | undefined; - let computeArgs: Record | undefined; - - // Parse braintrust.parent format: "project_id:abc", "project_name:xyz", or "experiment_id:123" - if (braintrustParent.startsWith("project_id:")) { - objectType = SpanObjectTypeV3.PROJECT_LOGS; - objectId = braintrustParent.substring("project_id:".length); - if (!objectId) { - console.error( - `parentFromHeaders: Invalid braintrust.parent format (empty project_id): ${braintrustParent}`, - ); - return undefined; - } - } else if (braintrustParent.startsWith("project_name:")) { - objectType = SpanObjectTypeV3.PROJECT_LOGS; - const projectName = braintrustParent.substring("project_name:".length); - if (!projectName) { - console.error( - `parentFromHeaders: Invalid braintrust.parent format (empty project_name): ${braintrustParent}`, - ); - return undefined; - } - computeArgs = { project_name: projectName }; - } else if (braintrustParent.startsWith("experiment_id:")) { - objectType = SpanObjectTypeV3.EXPERIMENT; - objectId = braintrustParent.substring("experiment_id:".length); - if (!objectId) { - console.error( - `parentFromHeaders: Invalid braintrust.parent format (empty experiment_id): ${braintrustParent}`, - ); - return undefined; - } - } else { - console.error( - `parentFromHeaders: Invalid braintrust.parent format: ${braintrustParent}. ` + - "Expected format: 'project_id:ID', 'project_name:NAME', or 'experiment_id:ID'", - ); - return undefined; - } - - // Create SpanComponentsV4 and export as string - const componentsData: { - object_type: number; - object_id?: string | null; - compute_object_metadata_args?: Record | null; - row_id: string; - span_id: string; - root_span_id: string; - } = { - object_type: objectType, - row_id: "otel", // Dummy row_id to enable span_id/root_span_id fields - span_id: spanIdHex, - root_span_id: traceIdHex, - }; - - // Add either object_id or compute_object_metadata_args, not both - if (computeArgs) { - componentsData.compute_object_metadata_args = computeArgs; - } else { - componentsData.object_id = objectId; - } - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any - const components = new SpanComponentsV4(componentsData as any); - - return components.toStr(); - } catch (error) { - console.error("parentFromHeaders: Error parsing headers:", error); - return undefined; - } -} - -function generateHexId(bytes: number): string { - let result = ""; - for (let i = 0; i < bytes; i++) { - result += Math.floor(Math.random() * 256) - .toString(16) - .padStart(2, "0"); - } - return result; -} - -/** - * ID generator that generates OpenTelemetry-compatible IDs - * Uses hex strings for compatibility with OpenTelemetry systems - */ -export class OTELIDGenerator extends IDGenerator { - getSpanId(): string { - // Generate 8 random bytes and convert to hex (16 characters) - return generateHexId(8); - } - - getTraceId(): string { - // Generate 16 random bytes and convert to hex (32 characters) - return generateHexId(16); - } - - shareRootSpanId(): boolean { - return false; - } +): PropagationContext | undefined { + return extractTraceContextFromHeaders(headers); } diff --git a/integrations/otel-js/tests/utils.ts b/integrations/otel-js/tests/utils.ts index 95d30b3b9..a435f7515 100644 --- a/integrations/otel-js/tests/utils.ts +++ b/integrations/otel-js/tests/utils.ts @@ -139,7 +139,20 @@ export function logOtelVersions(version: "v1" | "v2" | "parent"): void { * which is critical for testing with different OpenTelemetry versions. */ export function createOtelAliases(cwd: string): Record { + const repositoryRoot = + detectOtelVersion(cwd) === "parent" + ? path.resolve(cwd, "../..") + : path.resolve(cwd, "../../.."); return { + "@braintrust/otel": path.resolve( + repositoryRoot, + "integrations/otel-js/src/index.ts", + ), + "braintrust/instrumentation": path.resolve( + repositoryRoot, + "js/src/instrumentation/index.ts", + ), + braintrust: path.resolve(repositoryRoot, "js/src/node/index.ts"), "@opentelemetry/api": path.resolve(cwd, "node_modules/@opentelemetry/api"), "@opentelemetry/core": path.resolve( cwd, diff --git a/integrations/otel-js/tsconfig.json b/integrations/otel-js/tsconfig.json index 8cfc6d9ae..352d6b9f7 100644 --- a/integrations/otel-js/tsconfig.json +++ b/integrations/otel-js/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { "declaration": true, "lib": ["es2022"], - "module": "commonjs", + "module": "esnext", "target": "es2022", - "moduleResolution": "node", + "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, "esModuleInterop": true, diff --git a/integrations/otel-js/tsconfig.test.json b/integrations/otel-js/tsconfig.test.json index eff2845e0..ad0f1bdc1 100644 --- a/integrations/otel-js/tsconfig.test.json +++ b/integrations/otel-js/tsconfig.test.json @@ -6,7 +6,9 @@ "types": ["node"], "baseUrl": ".", "paths": { - "@braintrust/otel": ["src/index.ts"] + "@braintrust/otel": ["src/index.ts"], + "braintrust": ["../../js/src/node/index.ts"], + "braintrust/instrumentation": ["../../js/src/instrumentation/index.ts"] } }, "include": ["src/**/*.test.ts", "tests/**/*.ts"], diff --git a/integrations/otel-js/vitest.config.ts b/integrations/otel-js/vitest.config.ts index 5da49856a..bf4fb3a75 100644 --- a/integrations/otel-js/vitest.config.ts +++ b/integrations/otel-js/vitest.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vitest/config"; import { readFileSync } from "node:fs"; +import { createOtelAliases } from "./tests/utils"; const packageJson = JSON.parse( readFileSync(new URL("./package.json", import.meta.url), "utf8"), @@ -9,6 +10,7 @@ export default defineConfig({ define: { __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), }, + resolve: { alias: createOtelAliases(process.cwd()) }, test: { reporters: ["default"], include: ["tests/**/*.test.ts", "src/**/*.test.ts"], diff --git a/integrations/temporal-js/src/interceptors.ts b/integrations/temporal-js/src/interceptors.ts index 70706872d..64d09dfc7 100644 --- a/integrations/temporal-js/src/interceptors.ts +++ b/integrations/temporal-js/src/interceptors.ts @@ -6,15 +6,9 @@ import type { ActivityInterceptors, } from "@temporalio/worker"; import type { WorkflowClientInterceptor } from "@temporalio/client"; -import { defaultPayloadConverter } from "@temporalio/common"; import * as braintrust from "braintrust"; -import { SpanComponentsV4 } from "braintrust/util"; -import { getWorkflowSpanExport } from "./sinks"; -import { - BRAINTRUST_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_ID_HEADER, - deserializeHeaderValue, -} from "./utils"; +import { getWorkflowTraceContext } from "./sinks"; +import { deserializeTraceContext, serializeTraceContext } from "./utils"; /** * Create a client interceptor that propagates Braintrust span context to workflows. @@ -25,19 +19,13 @@ export function createBraintrustClientInterceptor(): WorkflowClientInterceptor { async start(input, next) { const span = braintrust.currentSpan(); if (span) { - const exported = await span.export(); - if (exported) { - const payload = defaultPayloadConverter.toPayload(exported); - if (payload) { - return next({ - ...input, - headers: { - ...input.headers, - [BRAINTRUST_SPAN_HEADER]: payload, - }, - }); - } - } + return next({ + ...input, + headers: { + ...input.headers, + ...serializeTraceContext(braintrust.injectTraceContext()), + }, + }); } return next(input); }, @@ -47,19 +35,13 @@ export function createBraintrustClientInterceptor(): WorkflowClientInterceptor { async signalWithStart(input, next) { const span = braintrust.currentSpan(); if (span) { - const exported = await span.export(); - if (exported) { - const payload = defaultPayloadConverter.toPayload(exported); - if (payload) { - return next({ - ...input, - headers: { - ...input.headers, - [BRAINTRUST_SPAN_HEADER]: payload, - }, - }); - } - } + return next({ + ...input, + headers: { + ...input.headers, + ...serializeTraceContext(braintrust.injectTraceContext()), + }, + }); } return next(input); }, @@ -79,76 +61,13 @@ class BraintrustActivityInterceptor implements ActivityInboundCallsInterceptor { const info = this.ctx.info; const runId = info.workflowExecution.runId; - // Try to get workflow span export - first check local Map, then headers - let parent: string | undefined; + // Prefer the live workflow span on the same worker, then fall back to the + // W3C context propagated through Temporal headers. + let parent: braintrust.PropagationContext | undefined; // Check if we have the workflow span export locally (same worker as workflow) - const spanExportPromise = getWorkflowSpanExport(runId); - if (spanExportPromise) { - try { - parent = await spanExportPromise; - } catch { - // Ignore errors, fall through to header check - } - } - - // For cross-worker activities: construct parent from workflow span ID + client context - if (!parent && input.headers) { - const workflowSpanId = deserializeHeaderValue( - input.headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER], - ); - const clientContext = deserializeHeaderValue( - input.headers[BRAINTRUST_SPAN_HEADER], - ); - - if (workflowSpanId && clientContext) { - try { - const clientComponents = SpanComponentsV4.fromStr(clientContext); - const clientData = clientComponents.data; - - // We can only construct a workflow parent if we have: - // 1. Tracing context (root_span_id) - // 2. Object metadata (object_id or compute_object_metadata_args) - const hasTracingContext = !!clientData.root_span_id; - const hasObjectMetadata = - !!clientData.object_id || !!clientData.compute_object_metadata_args; - - if (hasTracingContext && hasObjectMetadata) { - // Construct workflow parent with the workflow's span ID - // IMPORTANT: row_id must match span_id for the parent span - // Must provide EITHER object_id OR compute_object_metadata_args, not both - const workflowParentBase = { - object_type: clientData.object_type, - propagated_event: clientData.propagated_event, - row_id: workflowSpanId, // Use workflow's row_id, not client's - span_id: workflowSpanId, // Use workflow's span_id, not client's - root_span_id: clientData.root_span_id, // Keep same trace - }; - const workflowComponents = clientData.object_id - ? new SpanComponentsV4({ - ...workflowParentBase, - object_id: clientData.object_id, - }) - : new SpanComponentsV4({ - ...workflowParentBase, - compute_object_metadata_args: - clientData.compute_object_metadata_args!, - }); - - parent = workflowComponents.toStr(); - } else { - // Client context doesn't have root_span_id, use it directly - parent = clientContext; - } - } catch { - // Fall back to client context if parsing fails - parent = clientContext; - } - } else if (clientContext) { - // No workflow span ID, use client context directly - parent = clientContext; - } - } + parent = + getWorkflowTraceContext(runId) ?? deserializeTraceContext(input.headers); const span = braintrust.startSpan({ name: `temporal.activity.${info.activityType}`, diff --git a/integrations/temporal-js/src/sinks.ts b/integrations/temporal-js/src/sinks.ts index bd2967ca4..ee8dc8891 100644 --- a/integrations/temporal-js/src/sinks.ts +++ b/integrations/temporal-js/src/sinks.ts @@ -6,24 +6,26 @@ import * as braintrust from "braintrust"; // NOTE: WorkflowInfo is NOT included here - it's automatically injected by the runtime export interface BraintrustSinks extends Sinks { braintrust: { - workflowStarted(parentContext?: string, workflowSpanId?: string): void; + workflowStarted( + parentContext?: braintrust.PropagationContext, + workflowSpanId?: string, + ): void; workflowCompleted(error?: string): void; }; } // Active workflow spans tracked by run ID const workflowSpans = new Map(); -// Workflow span exports tracked by run ID (as promises for async export) -const workflowSpanExports = new Map>(); +const workflowTraceContexts = new Map(); /** * Get the exported span context for a workflow by run ID. * Activities on the same worker can use this to parent to the workflow span. */ -export function getWorkflowSpanExport( +export function getWorkflowTraceContext( runId: string, -): Promise | undefined { - return workflowSpanExports.get(runId); +): braintrust.PropagationContext | undefined { + return workflowTraceContexts.get(runId); } /** @@ -36,7 +38,7 @@ export function createBraintrustSinks(): InjectedSinks { workflowStarted: { fn: ( info: WorkflowInfo, - parentContext?: string, + parentContext?: braintrust.PropagationContext, workflowSpanId?: string, ) => { const span = braintrust.startSpan({ @@ -53,7 +55,7 @@ export function createBraintrustSinks(): InjectedSinks { }, }); workflowSpans.set(info.runId, span); - workflowSpanExports.set(info.runId, span.export()); + workflowTraceContexts.set(info.runId, span.inject()); }, callDuringReplay: false, }, @@ -66,7 +68,7 @@ export function createBraintrustSinks(): InjectedSinks { } span.end(); workflowSpans.delete(info.runId); - workflowSpanExports.delete(info.runId); + workflowTraceContexts.delete(info.runId); } }, callDuringReplay: false, diff --git a/integrations/temporal-js/src/temporal.test.ts b/integrations/temporal-js/src/temporal.test.ts index 04b9c43c0..f8a8732f4 100644 --- a/integrations/temporal-js/src/temporal.test.ts +++ b/integrations/temporal-js/src/temporal.test.ts @@ -2,15 +2,15 @@ import { expect, test, describe } from "vitest"; import { serializeHeaderValue, deserializeHeaderValue, - BRAINTRUST_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_ID_HEADER, + deserializeTraceContext, + serializeTraceContext, + withParentSpanId, } from "./utils"; import { SpanComponentsV3, SpanComponentsV4, SpanObjectTypeV3, -} from "braintrust/util"; +} from "../../../js/util"; import { BraintrustTemporalPlugin, createBraintrustTemporalPlugin, @@ -69,12 +69,29 @@ describe("temporal header utilities", () => { } }); - test("header constants are defined", () => { - expect(BRAINTRUST_SPAN_HEADER).toBe("_braintrust-span"); - expect(BRAINTRUST_WORKFLOW_SPAN_HEADER).toBe("_braintrust-workflow-span"); - expect(BRAINTRUST_WORKFLOW_SPAN_ID_HEADER).toBe( - "_braintrust-workflow-span-id", + test("round-trips W3C trace context through Temporal payloads", () => { + const context = { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + tracestate: "vendor=value", + baggage: "braintrust.parent=project_name%3Atest", + }; + expect(deserializeTraceContext(serializeTraceContext(context))).toEqual( + context, + ); + }); + + test("reparents W3C context to the workflow span", () => { + const context = withParentSpanId( + { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + baggage: "braintrust.parent=project_name%3Atest", + }, + "abcdefabcdefabcd", ); + expect(context).toEqual({ + traceparent: "00-12345678901234567890123456789012-abcdefabcdefabcd-01", + baggage: "braintrust.parent=project_name%3Atest", + }); }); }); diff --git a/integrations/temporal-js/src/utils.ts b/integrations/temporal-js/src/utils.ts index dc5add88a..51ffcae33 100644 --- a/integrations/temporal-js/src/utils.ts +++ b/integrations/temporal-js/src/utils.ts @@ -1,9 +1,7 @@ import type { Payload } from "@temporalio/common"; +import type { PropagationContext } from "braintrust"; -export const BRAINTRUST_SPAN_HEADER = "_braintrust-span"; -export const BRAINTRUST_WORKFLOW_SPAN_HEADER = "_braintrust-workflow-span"; -export const BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = - "_braintrust-workflow-span-id"; +const TRACE_CONTEXT_HEADERS = ["traceparent", "tracestate", "baggage"] as const; export function serializeHeaderValue(value: string): Payload { return { @@ -27,3 +25,41 @@ export function deserializeHeaderValue( return undefined; } } + +export function serializeTraceContext( + context: PropagationContext, +): Record { + return Object.fromEntries( + TRACE_CONTEXT_HEADERS.flatMap((name) => { + const value = context[name]; + return value ? [[name, serializeHeaderValue(value)]] : []; + }), + ); +} + +export function deserializeTraceContext( + headers: Record | undefined, +): PropagationContext | undefined { + if (!headers) return undefined; + const context = Object.fromEntries( + TRACE_CONTEXT_HEADERS.flatMap((name) => { + const value = deserializeHeaderValue(headers[name]); + return value ? [[name, value]] : []; + }), + ); + return context.traceparent ? context : undefined; +} + +export function withParentSpanId( + context: PropagationContext, + spanId: string, +): PropagationContext | undefined { + const match = context.traceparent.match( + /^([0-9a-f]{2})-([0-9a-f]{32})-[0-9a-f]{16}-([0-9a-f]{2})$/, + ); + if (!match) return undefined; + return { + ...context, + traceparent: `${match[1]}-${match[2]}-${spanId}-${match[3]}`, + }; +} diff --git a/integrations/temporal-js/src/workflow-interceptors.ts b/integrations/temporal-js/src/workflow-interceptors.ts index 914dcc154..b7bc0a2b8 100644 --- a/integrations/temporal-js/src/workflow-interceptors.ts +++ b/integrations/temporal-js/src/workflow-interceptors.ts @@ -12,7 +12,6 @@ import { WorkflowExecuteInput, Next, proxySinks, - workflowInfo, uuid4, } from "@temporalio/workflow"; import type { @@ -21,13 +20,12 @@ import type { StartChildWorkflowExecutionInput, } from "@temporalio/workflow"; import type { Payload } from "@temporalio/common"; +import type { PropagationContext } from "braintrust"; import type { BraintrustSinks } from "./sinks"; import { - BRAINTRUST_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_ID_HEADER, - serializeHeaderValue, - deserializeHeaderValue, + deserializeTraceContext, + serializeTraceContext, + withParentSpanId, } from "./utils"; const { braintrust } = proxySinks(); @@ -37,7 +35,7 @@ const { braintrust } = proxySinks(); * Created per-workflow by the factory function to avoid global state issues. */ interface WorkflowSpanState { - parentContext: string | undefined; + parentContext: PropagationContext | undefined; spanId: string | undefined; } @@ -49,15 +47,13 @@ class BraintrustWorkflowInboundInterceptor implements WorkflowInboundCallsInterc next: Next, ): Promise { // Extract parent context from headers - const parentContext = input.headers - ? deserializeHeaderValue(input.headers[BRAINTRUST_SPAN_HEADER]) - : undefined; + const parentContext = deserializeTraceContext(input.headers); // Store for the outbound interceptor to forward to activities this.state.parentContext = parentContext; // Generate a deterministic spanId for the workflow span - this.state.spanId = uuid4(); + this.state.spanId = uuid4().replace(/-/g, "").slice(0, 16); // Create workflow span via sink (only called if not replaying) // NOTE: WorkflowInfo is injected automatically by the runtime @@ -78,27 +74,11 @@ class BraintrustWorkflowOutboundInterceptor implements WorkflowOutboundCallsInte constructor(private state: WorkflowSpanState) {} private getHeaders(): Record { - const info = workflowInfo(); - const headers: Record = {}; - - // Pass runId so activity can look up workflow span on same worker - headers[BRAINTRUST_WORKFLOW_SPAN_HEADER] = serializeHeaderValue(info.runId); - - // Pass workflow span ID for cross-worker activities to construct parent - if (this.state.spanId) { - headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER] = serializeHeaderValue( - this.state.spanId, - ); - } - - // Pass client context for cross-worker activities to construct parent - if (this.state.parentContext) { - headers[BRAINTRUST_SPAN_HEADER] = serializeHeaderValue( - this.state.parentContext, - ); - } - - return headers; + const context = + this.state.parentContext && this.state.spanId + ? withParentSpanId(this.state.parentContext, this.state.spanId) + : undefined; + return context ? serializeTraceContext(context) : {}; } scheduleActivity( diff --git a/integrations/temporal-js/tsconfig.json b/integrations/temporal-js/tsconfig.json index 8cfc6d9ae..352d6b9f7 100644 --- a/integrations/temporal-js/tsconfig.json +++ b/integrations/temporal-js/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { "declaration": true, "lib": ["es2022"], - "module": "commonjs", + "module": "esnext", "target": "es2022", - "moduleResolution": "node", + "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, "esModuleInterop": true, diff --git a/integrations/temporal-js/tsup.config.ts b/integrations/temporal-js/tsup.config.ts index e37c36c4b..6563ce288 100644 --- a/integrations/temporal-js/tsup.config.ts +++ b/integrations/temporal-js/tsup.config.ts @@ -7,7 +7,6 @@ export default defineConfig([ outDir: "dist", external: [ "braintrust", - "braintrust/util", "@braintrust/temporal/workflow-interceptors", "@temporalio/activity", "@temporalio/client", diff --git a/integrations/vercel-ai-sdk/src/adapter.test.ts b/integrations/vercel-ai-sdk/src/adapter.test.ts index 14d9e698e..99fcb5ee7 100644 --- a/integrations/vercel-ai-sdk/src/adapter.test.ts +++ b/integrations/vercel-ai-sdk/src/adapter.test.ts @@ -1,8 +1,15 @@ -import { BraintrustStreamChunk } from "braintrust"; +import { BraintrustStream } from "braintrust"; import { ReadableStream, WritableStream } from "stream/web"; import { expect, test } from "vitest"; import { BraintrustAdapter } from "."; +type BraintrustStreamChunk = + ReturnType extends ReadableStream< + infer Chunk + > + ? Chunk + : never; + test("text callbacks", async () => { const textStream = new ReadableStream({ start(controller) { diff --git a/integrations/vercel-ai-sdk/src/adapter.ts b/integrations/vercel-ai-sdk/src/adapter.ts index 7bb36ffa4..ee1c9d92b 100644 --- a/integrations/vercel-ai-sdk/src/adapter.ts +++ b/integrations/vercel-ai-sdk/src/adapter.ts @@ -1,4 +1,4 @@ -import { BraintrustStream, BraintrustStreamChunk } from "braintrust"; +import { BraintrustStream } from "braintrust"; export interface AIStreamCallbacksAndOptions { onStart?: () => void | Promise; @@ -15,6 +15,13 @@ function formatStreamPart(type: "text" | "data", value: unknown): string { import { ReadableStream, TransformStream } from "stream/web"; +type BraintrustStreamChunk = + ReturnType extends ReadableStream< + infer Chunk + > + ? Chunk + : never; + export type BraintrustStreamOrReadable = | BraintrustStream | ReadableStream @@ -47,16 +54,6 @@ export function toDataStreamResponse( }); } -/** - * @deprecated Use `toDataStreamResponse` instead. - */ -export function toAIStreamResponse( - stream: BraintrustStreamOrReadable, - init?: ResponseInit, -): Response { - return toDataStreamResponse(stream, init); -} - function btStreamToAISDKTransformStream( callbacks?: AIStreamCallbacksAndOptions, ) { diff --git a/js/README.md b/js/README.md index da6da81a6..a350c1423 100644 --- a/js/README.md +++ b/js/README.md @@ -136,7 +136,7 @@ The wrappers are composable and idempotent. They preserve LangSmith behavior, in Disable LangSmith instrumentation in code or through the environment: ```typescript -import { configureInstrumentation } from "braintrust"; +import { configureInstrumentation } from "braintrust/instrumentation"; configureInstrumentation({ integrations: { langsmith: false } }); ``` @@ -153,7 +153,7 @@ When Braintrust LangChain/LangGraph instrumentation is enabled, LangSmith runs s See the [Migrate from v2.x to v3.x guide](https://www.braintrust.dev/docs/reference/sdks/typescript/migrations/v2-to-v3). -In 3.x, browser usage should move to `@braintrust/browser` instead of relying on the legacy `braintrust/browser` path. +For direct browser imports, use `@braintrust/browser`. The package root still selects its browser build through export conditions, but `braintrust/browser` is no longer published. ### Upgrading from 1.x to 2.x diff --git a/js/package.json b/js/package.json index 2f9375376..c62e2ce92 100644 --- a/js/package.json +++ b/js/package.json @@ -46,11 +46,6 @@ "require": "./dist/edge-light.js", "default": "./dist/edge-light.mjs" }, - "./browser": { - "import": "./dist/browser.mjs", - "require": "./dist/browser.js", - "default": "./dist/browser.mjs" - }, "./apply-auto-instrumentation": { "types": "./dist/apply-auto-instrumentation.d.ts", "edge-light": "./dist/apply-auto-instrumentation.browser.mjs", @@ -76,12 +71,6 @@ "module": "./dist/index.mjs", "require": "./dist/index.js" }, - "./util": { - "types": "./util/dist/index.d.ts", - "import": "./util/dist/index.mjs", - "module": "./util/dist/index.mjs", - "require": "./util/dist/index.js" - }, "./instrumentation": { "types": "./dist/instrumentation/index.d.ts", "import": "./dist/instrumentation/index.mjs", @@ -107,10 +96,6 @@ "module": "./dist/auto-instrumentations/bundler/next.mjs", "require": "./dist/auto-instrumentations/bundler/next.cjs" }, - "./webpack-loader": { - "types": "./dist/auto-instrumentations/bundler/webpack-loader.d.ts", - "require": "./dist/auto-instrumentations/bundler/webpack-loader.cjs" - }, "./esbuild": { "types": "./dist/auto-instrumentations/bundler/esbuild.d.ts", "import": "./dist/auto-instrumentations/bundler/esbuild.mjs", @@ -126,7 +111,6 @@ }, "files": [ "dist/**/*", - "util/dist/**/*", "bin/bt", "scripts/bt-helper.js", "scripts/install.js", @@ -138,7 +122,7 @@ "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsup", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "watch": "tsup --watch", - "clean": "rm -r dist/* && rm -r util/dist/*", + "clean": "rm -r dist/*", "docs": "typedoc --options typedoc.json src/node/index.ts", "test": "vitest run --exclude \"src/wrappers/**/*.test.ts\" --exclude \"src/otel/**/*.test.ts\" --exclude \"smoke/**/*.test.ts\" --exclude \"tests/api-compatibility/**\"", "test:core": "pnpm prune && pnpm test", diff --git a/js/smoke/scenarios/browser-main-package/README.md b/js/smoke/scenarios/browser-main-package/README.md index 1b78d3562..58a6f72fb 100644 --- a/js/smoke/scenarios/browser-main-package/README.md +++ b/js/smoke/scenarios/browser-main-package/README.md @@ -1,6 +1,6 @@ # Browser Main Package Smoke Test -This smoke test verifies that the informational message appears when using the browser build from the main `braintrust` package. +This smoke test verifies the curated browser API exposed by the main `braintrust` package. ## What This Tests @@ -10,11 +10,12 @@ When a user imports from the main `braintrust` package in a browser environment: import * as braintrust from "braintrust"; ``` -The bundler (via the `"browser"` field in package.json) will resolve to the browser build (`dist/browser.mjs`), which should: +The package's browser export condition resolves to `dist/browser.mjs`, which should: -1. Show an informational console message suggesting `@braintrust/browser` for optimal use -2. Provide working browser-safe implementations -3. Not include Node.js modules +1. Provide working browser-safe implementations +2. Expose the supported root API +3. Omit removed legacy and internal exports +4. Not include Node.js modules ## Test Structure @@ -44,21 +45,6 @@ pnpm exec playwright test ## What Gets Verified ✓ Import from main package works in browser -✓ Basic functions are available (init, newId, traceable) -✓ Informational message appears in console -✓ Message mentions "@braintrust/browser" package +✓ Supported functions are available (`init`, `flush`) +✓ Legacy exports such as `newId` and `traceable` are absent ✓ No Node.js module errors - -## Expected Console Output - -When the test runs, you should see: - -``` -Braintrust SDK Browser Build -You are using a browser-compatible build from the main package. -For optimal browser support consider: - npm install @braintrust/browser - import * as braintrust from "@braintrust/browser" -``` - -This message guides users toward the optimized `@braintrust/browser` package while ensuring the main package works correctly in browsers. diff --git a/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts b/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts index dd728e468..92f570a50 100644 --- a/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts +++ b/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts @@ -5,8 +5,8 @@ declare global { consoleMessages: string[]; importSuccessful: boolean; hasInit: boolean; - hasNewId: boolean; - hasTraceable: boolean; + hasFlush: boolean; + removedLegacyExports: boolean; }; } } @@ -22,13 +22,14 @@ console.info = (...args: any[]) => { // Import from main package browser export AFTER setting up console capture // This must be done dynamically to ensure console.info is overridden first -const braintrust = await import("braintrust/browser"); +const braintrust = await import("braintrust"); // Test that imports work const importSuccessful = true; const hasInit = typeof braintrust.init === "function"; -const hasNewId = typeof braintrust.newId === "function"; -const hasTraceable = typeof braintrust.traceable === "function"; +const hasFlush = typeof braintrust.flush === "function"; +const removedLegacyExports = + !("newId" in braintrust) && !("traceable" in braintrust); // Store results window.__btBrowserMessageTest = { @@ -36,8 +37,8 @@ window.__btBrowserMessageTest = { consoleMessages: capturedMessages, importSuccessful, hasInit, - hasNewId, - hasTraceable, + hasFlush, + removedLegacyExports, }; // Display results @@ -48,8 +49,8 @@ if (output) {
  • Import successful: ${importSuccessful ? "✓" : "✗"}
  • Has init function: ${hasInit ? "✓" : "✗"}
  • -
  • Has newId function: ${hasNewId ? "✓" : "✗"}
  • -
  • Has traceable function: ${hasTraceable ? "✓" : "✗"}
  • +
  • Has flush function: ${hasFlush ? "✓" : "✗"}
  • +
  • Legacy exports removed: ${removedLegacyExports ? "✓" : "✗"}
  • Console messages captured: ${capturedMessages.length}

Console Messages:

diff --git a/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts b/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts index 8c5d9fb7e..3a48a7f60 100644 --- a/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts +++ b/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; test.describe("Braintrust Main Package Browser Build", () => { - test("should display informational message when using browser build", async ({ + test("should expose the curated root API in a browser", async ({ page, baseURL, }) => { @@ -55,7 +55,7 @@ test.describe("Braintrust Main Package Browser Build", () => { expect(testResults.completed).toBe(true); expect(testResults.importSuccessful).toBe(true); expect(testResults.hasInit).toBe(true); - expect(testResults.hasNewId).toBe(true); - expect(testResults.hasTraceable).toBe(true); + expect(testResults.hasFlush).toBe(true); + expect(testResults.removedLegacyExports).toBe(true); }); }); diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs index 848eaef79..a5af1c383 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs @@ -86,7 +86,7 @@ async function testViteDevServerWithNodeEsm() { details: "Vite's dependency pre-bundler cannot handle Nunjucks' use of Object.setPrototypeOf when pre-bundling braintrust/node", recommendation: - "Use 'braintrust/browser' import or configure Vite to exclude Nunjucks from optimization", + "Import 'braintrust' through its browser condition or configure Vite to exclude Nunjucks from optimization", }; } @@ -163,7 +163,7 @@ async function testViteDevServerWithNodeEsm() { "Expected failure: Nunjucks incompatibility with Vite bundler when using braintrust/node. " + "Root cause: Nunjucks uses Object.setPrototypeOf in ways incompatible with Vite's ESM bundler. " + "The Node.js ESM build includes Nunjucks, which causes this error during Vite's dependency pre-bundling. " + - "Recommendation: Use 'braintrust/browser' import or exclude Nunjucks from Vite optimization", + "Recommendation: Import 'braintrust' through its browser condition or exclude Nunjucks from Vite optimization", error: errorStack ? { message: testResult.error, diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md index 2485bd93a..88af0b7e8 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md @@ -10,7 +10,7 @@ Tests whether the Braintrust SDK (browser build) can be loaded in Vite's dev ser **Hono framework:** Uses Hono for elegant routing instead of raw Worker API. -**Browser build:** Uses `braintrust/browser` import since Cloudflare Workers don't support Node.js APIs. +**Browser build:** Imports `braintrust`; the package's browser condition selects the browser-safe build because Cloudflare Workers don't support Node.js APIs. ## Test Suite diff --git a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md index 77b51ac01..de82ebdfb 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md +++ b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md @@ -10,7 +10,7 @@ Tests Braintrust SDK in a Cloudflare Workers environment with Vite bundling via **Hono framework:** Uses Hono for elegant routing instead of raw Worker API. -**Browser build:** Uses `braintrust/browser` import since Cloudflare Workers don't support Node.js APIs. +**Browser build:** Imports `braintrust`; the package's browser condition selects the browser-safe build because Cloudflare Workers don't support Node.js APIs. **Wrangler dev:** Tests via Wrangler dev server, which uses Vite to build the worker. diff --git a/js/smoke/scenarios/playwright-browser/src/browser-tests.ts b/js/smoke/scenarios/playwright-browser/src/browser-tests.ts index 403f1abc7..706b13cfd 100644 --- a/js/smoke/scenarios/playwright-browser/src/browser-tests.ts +++ b/js/smoke/scenarios/playwright-browser/src/browser-tests.ts @@ -44,15 +44,6 @@ const harness = createBrowserHarness(output); window.__btBrowserSmokeResults = harness.results; async function runAllTestSuites() { - if (!braintrust._exportsForTestingOnly) { - harness.fail( - "runtime", - "preflight", - new Error("_exportsForTestingOnly not available"), - ); - return; - } - harness.log("=== Running test suites ==="); const { passed, failed, xfail } = await runTests({ diff --git a/js/smoke/shared/src/helpers/register.ts b/js/smoke/shared/src/helpers/register.ts index 9b45d0126..9c0cce707 100644 --- a/js/smoke/shared/src/helpers/register.ts +++ b/js/smoke/shared/src/helpers/register.ts @@ -18,7 +18,10 @@ export interface TestContext { backgroundLogger: BackgroundLogger; } -export type TestFn = (braintrust: BraintrustModule) => Promise; +export type TestFn = ((braintrust: BraintrustModule) => Promise) & { + testName: string; + requiresTestHarness?: boolean; +}; type RegisteredTestFn = ( braintrust: BraintrustModule, @@ -35,24 +38,32 @@ type RegisteredTestFn = ( * - Return a partial TestResult -> merged with pass status * - Throw an error -> fail with error details */ -export function register(name: string, fn: RegisteredTestFn): TestFn { +export function register( + name: string, + fn: RegisteredTestFn, + { requiresTestHarness = false }: { requiresTestHarness?: boolean } = {}, +): TestFn { registeredTests.add(name); - return async (braintrust: BraintrustModule): Promise => { - const testing = braintrust._exportsForTestingOnly as { - setInitialTestState: () => void; - simulateLoginForTests: () => Promise | unknown; - simulateLogoutForTests?: () => Promise | unknown; - useTestBackgroundLogger: () => BackgroundLogger; - clearTestBackgroundLogger: () => void; - }; + const test = async (braintrust: BraintrustModule): Promise => { + const testing = braintrust._exportsForTestingOnly as + | { + setInitialTestState: () => void; + simulateLoginForTests: () => Promise | unknown; + simulateLogoutForTests?: () => Promise | unknown; + useTestBackgroundLogger: () => BackgroundLogger; + clearTestBackgroundLogger: () => void; + } + | undefined; - testing.setInitialTestState(); - await testing.simulateLoginForTests(); - const backgroundLogger = testing.useTestBackgroundLogger(); + testing?.setInitialTestState(); + await testing?.simulateLoginForTests(); + const backgroundLogger = testing?.useTestBackgroundLogger(); try { - const result = await fn(braintrust, { backgroundLogger }); + const result = await fn(braintrust, { + backgroundLogger: backgroundLogger!, + }); if (typeof result === "string") { return { status: "pass", name, message: result }; @@ -71,12 +82,15 @@ export function register(name: string, fn: RegisteredTestFn): TestFn { }, }; } finally { - testing.clearTestBackgroundLogger(); - if (typeof testing.simulateLogoutForTests === "function") { + testing?.clearTestBackgroundLogger(); + if (typeof testing?.simulateLogoutForTests === "function") { await testing.simulateLogoutForTests(); } } }; + test.testName = name; + test.requiresTestHarness = requiresTestHarness; + return test; } /** @@ -146,9 +160,21 @@ export async function runTests({ skipCoverage = false, }: RunTestsOptions): Promise { const results: TestResult[] = []; + const testModule: BraintrustModule = { + ...braintrust, + __publicModule: braintrust, + }; for (const test of tests) { - results.push(await test(braintrust)); + if (!testModule._exportsForTestingOnly && test.requiresTestHarness) { + results.push({ + status: "xfail", + name: test.testName, + message: "Requires the private SDK test harness", + }); + } else { + results.push(await test(testModule)); + } } if (!skipCoverage) { @@ -186,7 +212,7 @@ export function expectFailure( predicate: ErrorPredicate, reason: string, ): TestFn { - return async (braintrust: BraintrustModule): Promise => { + const wrapped = async (braintrust: BraintrustModule): Promise => { const result = await test(braintrust); if (result.status === "fail" && result.error) { @@ -200,4 +226,7 @@ export function expectFailure( } return result; }; + wrapped.testName = test.testName; + wrapped.requiresTestHarness = test.requiresTestHarness; + return wrapped; } diff --git a/js/smoke/shared/src/suites/basic-logging.ts b/js/smoke/shared/src/suites/basic-logging.ts index 9bd174022..1a92af9d4 100644 --- a/js/smoke/shared/src/suites/basic-logging.ts +++ b/js/smoke/shared/src/suites/basic-logging.ts @@ -47,6 +47,7 @@ export const testBasicSpanLogging = register( return "Basic span logging test passed"; }, + { requiresTestHarness: true }, ); export const testMultipleSpans = register( @@ -78,6 +79,7 @@ export const testMultipleSpans = register( return `Multiple spans test passed (${events.length} events captured)`; }, + { requiresTestHarness: true }, ); export const testDirectLogging = register( @@ -102,6 +104,7 @@ export const testDirectLogging = register( return "Direct logging test passed"; }, + { requiresTestHarness: true }, ); export const testJSONAttachment = register( @@ -159,6 +162,7 @@ export const testJSONAttachment = register( return "JSONAttachment test passed"; }, + { requiresTestHarness: true }, ); export const testAsyncLocalStorageTraced = register( @@ -221,6 +225,7 @@ export const testAsyncLocalStorageTraced = register( ); } }, + { requiresTestHarness: true }, ); export const testNestedTraced = register( @@ -301,6 +306,7 @@ export const testNestedTraced = register( return "ALS not available in this environment, test skipped"; } }, + { requiresTestHarness: true }, ); export const testCurrentSpan = register( @@ -355,4 +361,5 @@ export const testCurrentSpan = register( throw new Error("currentSpan() returned a span ID that was not logged"); } }, + { requiresTestHarness: true }, ); diff --git a/js/smoke/shared/src/suites/import-verification.ts b/js/smoke/shared/src/suites/import-verification.ts index cf422006b..220ff39b5 100644 --- a/js/smoke/shared/src/suites/import-verification.ts +++ b/js/smoke/shared/src/suites/import-verification.ts @@ -24,6 +24,7 @@ import { register, type TestFn } from "../helpers/register"; * Interface for the Braintrust module based on exports.ts */ export interface BraintrustModule { + __publicModule?: BraintrustModule; // Core logging (REQUIRED) initLogger?: unknown; Logger?: unknown; @@ -87,10 +88,8 @@ export interface BraintrustModule { projects?: unknown; PromptBuilder?: unknown; - // ID Generation (REQUIRED) + // Legacy private smoke-test harness fields IDGenerator?: unknown; - - // Testing (REQUIRED) _exportsForTestingOnly?: unknown; // State management (REQUIRED) @@ -126,13 +125,30 @@ export const testCoreLoggingExports = register( assertDefined(module.startSpan, "startSpan must exist"); assertType(module.startSpan, "function", "startSpan must be a function"); - assertDefined(module.log, "log must exist"); - assertType(module.log, "function", "log must be a function"); - assertDefined(module.flush, "flush must exist"); assertType(module.flush, "function", "flush must be a function"); - return "Core logging exports verified (7 exports)"; + const publicModule = module.__publicModule ?? module; + for (const removed of [ + "default", + "log", + "summarize", + "initExperiment", + "traceable", + "EvalResultWithSummary", + "SpanImpl", + "_exportsForTestingOnly", + "AttachmentReference", + "promptDefinitionSchema", + "braintrustEveInstrumentation", + "braintrustFlueInstrumentation", + ]) { + if (removed in publicModule) { + throw new Error(`${removed} must not be publicly exported`); + } + } + + return "Core logging exports and removals verified"; }, ); @@ -176,12 +192,8 @@ export const testPromptExports = register( export const testExperimentExports = register( "testExperimentExports", async (module) => { - assertDefined(module.initExperiment, "initExperiment must exist"); - assertType( - module.initExperiment, - "function", - "initExperiment must be a function", - ); + assertDefined(module.init, "init must exist"); + assertType(module.init, "function", "init must be a function"); assertDefined(module.Experiment, "Experiment must exist"); assertType( @@ -205,16 +217,6 @@ export const testEvalExports = register("testEvalExports", async (module) => { assertDefined(module.Eval, "Eval must exist"); assertType(module.Eval, "function", "Eval must be a function"); - assertDefined( - module.EvalResultWithSummary, - "EvalResultWithSummary must exist", - ); - assertType( - module.EvalResultWithSummary, - "function", - "EvalResultWithSummary must be a function/class", - ); - assertDefined(module.Reporter, "Reporter must exist"); assertType(module.Reporter, "function", "Reporter must be a function"); @@ -228,7 +230,7 @@ export const testEvalExports = register("testEvalExports", async (module) => { "defaultErrorScoreHandler must be a function", ); - return "Eval exports verified (4 runtime exports)"; + return "Eval exports verified (3 runtime exports)"; }); export const testTracingExports = register( @@ -237,9 +239,6 @@ export const testTracingExports = register( assertDefined(module.traced, "traced must exist"); assertType(module.traced, "function", "traced must be a function"); - assertDefined(module.traceable, "traceable must exist"); - assertType(module.traceable, "function", "traceable must be a function"); - assertDefined(module.wrapTraced, "wrapTraced must exist"); assertType(module.wrapTraced, "function", "wrapTraced must be a function"); @@ -260,7 +259,7 @@ export const testTracingExports = register( "withCurrent must be a function", ); - return "Tracing exports verified (6 exports)"; + return "Tracing exports verified (5 exports)"; }, ); @@ -276,7 +275,6 @@ export const testClientWrapperExports = register( "wrapGoogleGenAI", "wrapOpenRouter", "wrapAISDK", - "wrapMastraAgent", "wrapClaudeAgentSDK", ]; @@ -312,13 +310,7 @@ export const testUtilityExports = register( "Attachment must be a function/class", ); - assertDefined(module.newId, "newId must exist"); - assertType(module.newId, "function", "newId must be a function"); - - assertDefined(module.permalink, "permalink must exist"); - assertType(module.permalink, "function", "permalink must be a function"); - - return "Utility exports verified (4 exports)"; + return "Attachment exports verified (2 exports)"; }, ); @@ -342,20 +334,10 @@ export const testFunctionExports = register( export const testFramework2Exports = register( "testFramework2Exports", async (module) => { - assertDefined(module.Project, "Project must exist"); - assertType(module.Project, "function", "Project must be a function/class"); - assertDefined(module.projects, "projects must exist"); assertType(module.projects, "object", "projects must be an object"); - assertDefined(module.PromptBuilder, "PromptBuilder must exist"); - assertType( - module.PromptBuilder, - "function", - "PromptBuilder must be a function/class", - ); - - return "Framework2 exports verified (3 exports)"; + return "Project facade export verified"; }, ); @@ -369,8 +351,9 @@ export const testIDGeneratorExports = register( "IDGenerator must be a function/class", ); - return "ID generator exports verified (1 export)"; + return "Internal ID generator export verified"; }, + { requiresTestHarness: true }, ); export const testTestingExports = register( @@ -386,8 +369,9 @@ export const testTestingExports = register( "_exportsForTestingOnly must be an object", ); - return "Testing exports verified (1 export)"; + return "Internal testing export verified"; }, + { requiresTestHarness: true }, ); export const testStateManagementExports = register( @@ -415,34 +399,39 @@ export function testBuildResolution( | "workerd" | "node", ): TestFn { - return register("testBuildResolution", async (module) => { - const { buildType: detectedBuild, buildDetails } = detectBuildType(module); - const detectedFormat = detectModuleFormat(); - - if (detectedBuild === "unknown") { - throw new Error( - `Build type is unknown - configureBrowser() or configureNode() was not called. ${buildDetails || ""}`, - ); - } + return register( + "testBuildResolution", + async (module) => { + const { buildType: detectedBuild, buildDetails } = + detectBuildType(module); + const detectedFormat = detectModuleFormat(); + + if (detectedBuild === "unknown") { + throw new Error( + `Build type is unknown - configureBrowser() or configureNode() was not called. ${buildDetails || ""}`, + ); + } - if (expectedBuildType && detectedBuild !== expectedBuildType) { - throw new Error( - `Expected build type "${expectedBuildType}" but detected "${detectedBuild}"`, - ); - } + if (expectedBuildType && detectedBuild !== expectedBuildType) { + throw new Error( + `Expected build type "${expectedBuildType}" but detected "${detectedBuild}"`, + ); + } - const parts: string[] = [`Detected ${detectedBuild} build`]; + const parts: string[] = [`Detected ${detectedBuild} build`]; - if (expectedBuildType) { - parts.push(`(expected: ${expectedBuildType})`); - } + if (expectedBuildType) { + parts.push(`(expected: ${expectedBuildType})`); + } - if (detectedFormat !== "unknown") { - parts.push(`${detectedFormat} format`); - } + if (detectedFormat !== "unknown") { + parts.push(`${detectedFormat} format`); + } - return parts.join(", "); - }); + return parts.join(", "); + }, + { requiresTestHarness: true }, + ); } function detectBuildType(module: BraintrustModule): { diff --git a/js/src/auto-instrumentations/bundler/esbuild.ts b/js/src/auto-instrumentations/bundler/esbuild.ts index dc1443ad8..9e96a2c90 100644 --- a/js/src/auto-instrumentations/bundler/esbuild.ts +++ b/js/src/auto-instrumentations/bundler/esbuild.ts @@ -7,10 +7,3 @@ export function braintrustEsbuildPlugin( ): EsbuildPlugin { return unplugin.esbuild(options); } - -export type EsbuildPluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustEsbuildPlugin} instead. - */ -export const esbuildPlugin = unplugin.esbuild; diff --git a/js/src/auto-instrumentations/bundler/next.ts b/js/src/auto-instrumentations/bundler/next.ts index 774a4ab98..286be8f99 100644 --- a/js/src/auto-instrumentations/bundler/next.ts +++ b/js/src/auto-instrumentations/bundler/next.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; -import { join } from "node:path"; -import { webpackPlugin } from "./webpack"; +import { dirname, join } from "node:path"; +import { braintrustWebpackPlugin } from "./webpack"; type MaybePromise = T | Promise; type NextConfigFunction = ( @@ -144,7 +144,7 @@ function wrapWebpackConfig( : "server" : "client"; - const plugin = webpackPlugin({ + const plugin = braintrustWebpackPlugin({ browser: runtime === "client" || runtime === "edge", }); @@ -233,11 +233,11 @@ function addBraintrustTurbopackRule( } function getWebpackLoaderPath(): string { - try { - return requireFromProject.resolve("braintrust/webpack-loader"); - } catch { - return "braintrust/webpack-loader"; - } + const packageJsonPath = requireFromProject.resolve("braintrust/package.json"); + return join( + dirname(packageJsonPath), + "dist/auto-instrumentations/bundler/webpack-loader.cjs", + ); } function getNextMajorVersion(): number | undefined { diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 47562db0d..98361aaea 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -24,17 +24,6 @@ export interface BundlerPluginOptions { * @default false */ browser?: boolean; - - /** - * Marks transformed source as targeting a browser or edge-like environment. - * - * This retains the previous browser-target behavior of the option. Global - * instrumentation hooks are runtime-independent, so no diagnostics-channel - * compatibility shim is injected. - * - * @deprecated Use `browser` instead. - */ - useDiagnosticChannelCompatShim?: boolean; } /** @@ -55,8 +44,7 @@ function getModuleVersion(basedir: string): string | undefined { } export const unplugin = createUnplugin((options = {}) => { - const browser = - options.browser ?? options.useDiagnosticChannelCompatShim ?? false; + const browser = options.browser ?? false; const allInstrumentations = getDefaultAutoInstrumentationConfigs( options.instrumentations, ); diff --git a/js/src/auto-instrumentations/bundler/rollup.ts b/js/src/auto-instrumentations/bundler/rollup.ts index fc93bb614..a27de3589 100644 --- a/js/src/auto-instrumentations/bundler/rollup.ts +++ b/js/src/auto-instrumentations/bundler/rollup.ts @@ -7,10 +7,3 @@ export function braintrustRollupPlugin( ): RollupPlugin | RollupPlugin[] { return unplugin.rollup(options); } - -export type RollupPluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustRollupPlugin} instead. - */ -export const rollupPlugin = unplugin.rollup; diff --git a/js/src/auto-instrumentations/bundler/vite.ts b/js/src/auto-instrumentations/bundler/vite.ts index 297949eff..51f0e66d2 100644 --- a/js/src/auto-instrumentations/bundler/vite.ts +++ b/js/src/auto-instrumentations/bundler/vite.ts @@ -39,10 +39,3 @@ export function braintrustVitePlugin( ...(Array.isArray(transformPlugin) ? transformPlugin : [transformPlugin]), ]; } - -export type VitePluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustVitePlugin} instead. - */ -export const vitePlugin = unplugin.vite; diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 211cd31a5..80874304e 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -4,21 +4,8 @@ * This is a webpack loader (not a plugin) for compatibility with tools that only support loaders, * such as Next.js Turbopack. * - * Usage in next.config.js / next.config.ts: - * ```javascript - * const nextConfig: NextConfig = { - * turbopack: { - * rules: { - * // Apply the loader to all JS/MJS/CJS files from node_modules. - * // condition: "foreign" restricts the rule to third-party packages only. - * "*.{js,mjs,cjs}": { - * condition: "foreign", - * loaders: [{ loader: require.resolve("braintrust/webpack-loader") }], - * }, - * }, - * }, - * }; - * ``` + * The `braintrust/next` entrypoint resolves this implementation directly when + * configuring Turbopack; it is not a user-facing package entrypoint. */ import { create } from "../orchestrion-js"; @@ -130,7 +117,7 @@ function codeTransformerLoader( modulePath: normalizedModulePath, source: code, format: isModule ? "esm" : "cjs", - browser: options.browser ?? options.useDiagnosticChannelCompatShim ?? false, + browser: options.browser ?? false, }); if (patched !== null) { return callback(null, patched); diff --git a/js/src/auto-instrumentations/bundler/webpack.ts b/js/src/auto-instrumentations/bundler/webpack.ts index b81da8456..0617ee9bc 100644 --- a/js/src/auto-instrumentations/bundler/webpack.ts +++ b/js/src/auto-instrumentations/bundler/webpack.ts @@ -7,10 +7,3 @@ export function braintrustWebpackPlugin( ): WebpackPluginInstance { return unplugin.webpack(options); } - -export type WebpackPluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustWebpackPlugin} instead. - */ -export const webpackPlugin = unplugin.webpack; diff --git a/js/src/auto-instrumentations/configs/all.ts b/js/src/auto-instrumentations/configs/all.ts index 3b42cb11a..c576eaccf 100644 --- a/js/src/auto-instrumentations/configs/all.ts +++ b/js/src/auto-instrumentations/configs/all.ts @@ -47,11 +47,11 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] }, { integrations: ["anthropic"], configs: anthropicConfigs }, { - integrations: ["bedrock", "awsBedrock", "awsBedrockRuntime"], + integrations: ["awsBedrockRuntime"], configs: bedrockRuntimeConfigs, }, { - integrations: ["aisdk", "vercel"], + integrations: ["aisdk"], configs: aiSDKConfigs, }, { @@ -70,13 +70,13 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] integrations: ["cloudflareThink"], configs: cloudflareThinkConfigs, }, - { integrations: ["cursor", "cursorSDK"], configs: cursorSDKConfigs }, + { integrations: ["cursorSDK"], configs: cursorSDKConfigs }, { integrations: ["openAIAgents"], configs: openAIAgentsCoreConfigs, }, { - integrations: ["google", "googleGenAI"], + integrations: ["googleGenAI"], configs: googleGenAIConfigs, }, { diff --git a/js/src/browser/index.ts b/js/src/browser/index.ts index f478444b7..dfbca964f 100644 --- a/js/src/browser/index.ts +++ b/js/src/browser/index.ts @@ -14,4 +14,3 @@ import { configureBrowser } from "./config"; configureBrowser(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/edge-light/index.ts b/js/src/edge-light/index.ts index 4c6ef7bbf..800419f2a 100644 --- a/js/src/edge-light/index.ts +++ b/js/src/edge-light/index.ts @@ -10,4 +10,3 @@ import { configureEdgeLight } from "./config"; configureEdgeLight(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/edge-runtime-bootstrap.test.ts b/js/src/edge-runtime-bootstrap.test.ts index 39f772620..7cfce0fa8 100644 --- a/js/src/edge-runtime-bootstrap.test.ts +++ b/js/src/edge-runtime-bootstrap.test.ts @@ -67,19 +67,19 @@ describe.each([ }); const braintrust = await import(entrypoint); + const { _exportsForTestingOnly } = await import("./logger"); - braintrust._exportsForTestingOnly.setInitialTestState(); - await braintrust._exportsForTestingOnly.simulateLoginForTests(); + _exportsForTestingOnly.setInitialTestState(); + await _exportsForTestingOnly.simulateLoginForTests(); - const backgroundLogger = - braintrust._exportsForTestingOnly.useTestBackgroundLogger(); + const backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); const logger = braintrust.initLogger({ projectId: "test-project-id", projectName, }); const root = logger.startSpan({ name: "root", type: "task" }); - const parent = await root.export(); + const parent = braintrust.extractTraceContextFromHeaders(root.inject())!; const result = await braintrust.traced( async (span: unknown) => { @@ -113,12 +113,12 @@ describe.each([ test("wrapAISDK logs spans in edge runtimes", async () => { const braintrust = await import(entrypoint); + const { _exportsForTestingOnly } = await import("./logger"); - braintrust._exportsForTestingOnly.setInitialTestState(); - await braintrust._exportsForTestingOnly.simulateLoginForTests(); + _exportsForTestingOnly.setInitialTestState(); + await _exportsForTestingOnly.simulateLoginForTests(); - const backgroundLogger = - braintrust._exportsForTestingOnly.useTestBackgroundLogger(); + const backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); braintrust.initLogger({ projectId: "test-project-id", projectName, diff --git a/js/src/exports.ts b/js/src/exports.ts index ffce08390..d08f492e5 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -1,210 +1,79 @@ -import type { z } from "zod/v3"; -import { AttachmentReference as attachmentReferenceSchema } from "./generated_types"; -import type { AttachmentReferenceType } from "./generated_plain_types"; +/** + * The public `braintrust` package surface. + * + * Keep this file as an explicit allowlist. Implementation details should stay + * private; integrations belong on narrow, intentional extension points. + */ export type { - AnyDataset, - AttachmentParams, - BackgroundLoggerOpts, - BaseMetadata, - ChatPrompt, - CompiledPrompt, - CompiledPromptParams, - CompletionPrompt, ContextParentSpanIds, - DatasetRestorePreviewResult, - DatasetRestoreResult, - DatasetSnapshot, - DataSummary, - DatasetSummary, - DefaultMetadataType, - DefaultPromptArgs, - EndSpanArgs, - EvalCase, - ExperimentSummary, - Exportable, - ExternalAttachmentParams, - FullInitDatasetOptions, - FullInitOptions, - FullLoginOptions, - InitDatasetOptions, - InitLoggerOptions, - InitOptions, - LoadPromptOptions, - LoginOptions, - Logs3OverflowInputRow, - Logs3OverflowUpload, - MetricSummary, - ObjectMetadata, + CurrentSpanStore, PropagationContext, - PromptRowWithId, - ScoreSummary, - SerializedBraintrustState, - SetCurrentArg, Span, StartSpanArgs, - WithTransactionId, } from "./logger"; - -export type { - SandboxConfig, - RegisterSandboxOptions, - RegisterSandboxResult, -} from "./sandbox"; - export { Attachment, BaseAttachment, - BRAINTRUST_CURRENT_SPAN_STORE, BraintrustState, - ContextManager, - CurrentSpanStore, - DEFAULT_FETCH_BATCH_SIZE, - DEFAULT_MAX_REQUEST_SIZE, Dataset, - ObjectFetcher, - ERR_PERMALINK, Experiment, ExternalAttachment, - FailedHTTPResponse, JSONAttachment, - LOGS3_OVERFLOW_REFERENCE_TYPE, Logger, - LoginInvalidOrgError, NOOP_SPAN, - NOOP_SPAN_PERMALINK, - NoopSpan, Prompt, ReadonlyAttachment, ReadonlyExperiment, - SpanImpl, - TestBackgroundLogger, - _exportsForTestingOnly, - _internalGetGlobalState, - _internalSetInitialState, - constructLogs3OverflowRequest, currentExperiment, currentLogger, currentSpan, - deepCopyEvent, - deserializePlainStringAsJSON, + configureContextManager, + ContextManager, extractTraceContextFromHeaders, flush, - getContextManager, getPromptVersions, - getSpanParentObject, init, injectTraceContext, initDataset, - initExperiment, initLogger, loadParameters, loadPrompt, - log, logError, login, loginToState, - logs3OverflowUploadSchema, - newId, - permalink, - pickLogs3OverflowObjectIds, - uploadLogs3OverflowPayload, - utf8ByteLength, - renderMessage, - renderPromptParams, setFetch, setMaskingFunction, - spanComponentsToObjectId, startSpan, - summarize, - traceable, traced, updateSpan, withCurrent, - withDataset, - withExperiment, withParent, wrapTraced, - registerOtelFlush, } from "./logger"; export { registerSandbox } from "./sandbox"; -// Internal isomorph layer for platform-specific implementations -import _internalIso from "./isomorph"; -export { _internalIso }; - +export type { TemplateRendererPlugin } from "./template/registry"; +export { registerTemplatePlugin } from "./template/registry"; export { isTemplateFormat, parseTemplateFormat, renderTemplateContent, } from "./template/renderer"; -export type { TemplateFormat } from "./template/registry"; -export type { - TemplateRenderer, - TemplateRendererPlugin, -} from "./template/registry"; -export { - registerTemplatePlugin, - getTemplateRenderer, - templateRegistry, -} from "./template/registry"; - -export type { InvokeFunctionArgs, InvokeReturn } from "./functions/invoke"; export { initFunction, invoke } from "./functions/invoke"; +export { BraintrustStream } from "./functions/stream"; -export type { BraintrustStreamChunk } from "./functions/stream"; -export { - BraintrustStream, - braintrustStreamChunkSchema, - createFinalValuePassThroughStream, - devNullWritableStream, -} from "./functions/stream"; - -export { - IDGenerator, - UUIDGenerator, - OTELIDGenerator, - getIdGenerator, -} from "./id-gen"; - -export { - TRACEPARENT_HEADER, - TRACESTATE_HEADER, - BAGGAGE_HEADER, - BRAINTRUST_PARENT_KEY, -} from "./propagation"; -export type { - ParsedTraceparent, - PropagatedState, - TraceContextCarrier, - TraceContextHeaders, -} from "./propagation"; - -export { - LEGACY_CACHED_HEADER, - X_CACHED_HEADER, - parseCachedHeader, - wrapOpenAI, - wrapOpenAIv4, -} from "./wrappers/oai"; - +export { wrapOpenAI } from "./wrappers/oai"; export { braintrustAISDKTelemetry, wrapAISDK, wrapAgentClass, - BraintrustMiddleware, - wrapAISDKModel, } from "./wrappers/ai-sdk"; -export { braintrustEveInstrumentation } from "./instrumentation"; export { collectAnthropicSession } from "./wrappers/anthropic-session-collector"; export { wrapAnthropic } from "./wrappers/anthropic"; -export { - BraintrustObservabilityExporter, - wrapMastraAgent, -} from "./wrappers/mastra"; -export type { MastraObservabilityExporter } from "./wrappers/mastra"; +export { BraintrustObservabilityExporter } from "./wrappers/mastra"; export { wrapClaudeAgentSDK } from "./wrappers/claude-agent-sdk/claude-agent-sdk"; export { wrapCloudflareThink } from "./wrappers/cloudflare-think"; export { wrapOpenAICodexSDK } from "./wrappers/openai-codex"; @@ -234,37 +103,24 @@ export { } from "./wrappers/langsmith"; export { wrapVitest } from "./wrappers/vitest"; export { initNodeTestSuite } from "./wrappers/node-test"; -export { - BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME, - BraintrustLangChainCallbackHandler, -} from "./wrappers/langchain/callback-handler"; +export { BraintrustLangChainCallbackHandler } from "./wrappers/langchain/callback-handler"; export type { LangChainCallbackHandlerOptions } from "./vendor-sdk-types/langchain"; -export * as graph from "./graph-framework"; - export type { Evaluator, EvalTask, - EvalHooks, - EvalResult, - EvalScorerArgs, EvalScorer, EvalClassifier, - EvaluatorDef, ReporterBody, - SpanContext, } from "./framework"; - export { BaseExperiment, Eval, - EvalResultWithSummary, Reporter, defaultErrorScoreHandler, } from "./framework"; export type { DurableEvalStore } from "./durable-eval"; - export { BatchScorer, BatchTask, @@ -274,61 +130,8 @@ export { } from "./durable-eval"; export { agentAssertionScorer } from "./agent-assertions"; - export { DatasetPipeline } from "./dataset-pipeline"; +export { projects } from "./framework2"; -export type { - CodeOpts, - CreateProjectOpts, - PromptOpts, - ScorerOpts, -} from "./framework2"; - -export { - CodeFunction, - CodePrompt, - Project, - PromptBuilder, - ScorerBuilder, - ToolBuilder, - projects, -} from "./framework2"; - -export { - promptContentsSchema, - promptDefinitionSchema, - promptDefinitionToPromptData, - promptDefinitionWithToolsSchema, - PromptContents, - PromptDefinition, - PromptDefinitionWithTools, -} from "./prompt-schemas"; - -export type { Trace, SpanData, GetThreadOptions } from "./trace"; -export { SpanFetcher, CachedSpanFetcher, LocalTrace } from "./trace"; - -export type { - ParentExperimentIds, - ParentProjectLogIds, - IdField, - InputField, - OtherExperimentLogFields, - ExperimentLogPartialArgs, - ExperimentLogFullArgs, - LogFeedbackFullArgs, - LogCommentFullArgs, - CommentEvent, - DatasetRecord, -} from "../util"; - -export { addAzureBlobHeaders, LazyValue } from "./util"; - -export const AttachmentReference: z.ZodType = - attachmentReferenceSchema; - -export type { EvalParameters } from "./eval-parameters"; - -// Auto-instrumentation configuration -export { configureInstrumentation } from "./instrumentation"; -export { braintrustFlueInstrumentation } from "./instrumentation"; -export type { InstrumentationConfig } from "./instrumentation"; +export type { Trace } from "./trace"; +export { LocalTrace } from "./trace"; diff --git a/js/src/framework.test.ts b/js/src/framework.test.ts index cb905aad7..a61b578c2 100644 --- a/js/src/framework.test.ts +++ b/js/src/framework.test.ts @@ -45,10 +45,8 @@ test("meta (write) is passed to task", async () => { projectName: "proj", evalName: "eval", data: [{ input: 1, metadata }], - task: async (input: number, { meta }) => { - meta({ - foo: "barbar", - }); + task: async (input: number, { metadata }) => { + metadata.foo = "barbar"; return input * 2; }, scores: [], diff --git a/js/src/framework.ts b/js/src/framework.ts index 540b95dc6..9d2bef9d3 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -42,7 +42,7 @@ import { startSpan, traced, withCurrent, - withParent, + _internalWithParent, _internalGetGlobalState, } from "./logger"; import type { ProgressReporter } from "./reporters/types"; @@ -131,10 +131,6 @@ export interface EvalHooks< Metadata extends BaseMetadata, Parameters extends EvalParameters, > { - /** - * @deprecated Use `metadata` instead. - */ - meta: (info: Metadata) => void; /** * The metadata object for the current evaluation. You can mutate this object to add or remove metadata. */ @@ -371,37 +367,14 @@ export interface Evaluator< flushBeforeScoring?: boolean; } -export class EvalResultWithSummary< +export interface EvalResultWithSummary< Input, Output, Expected, Metadata extends BaseMetadata = DefaultMetadataType, > { - constructor( - public summary: ExperimentSummary, - public results: EvalResult[], - ) {} - - /** - * @deprecated Use `summary` instead. - */ - toString(): string { - return JSON.stringify(this.summary); - } - - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `EvalResultWithSummary(summary="...", results=[...])`; - } - - toJSON(): { - summary: ExperimentSummary; - results: EvalResult[]; - } { - return { - summary: this.summary, - results: this.results, - }; - } + summary: ExperimentSummary; + results: EvalResult[]; } export type { ReporterBody } from "./reporters/types"; @@ -552,7 +525,7 @@ export function callEvaluatorData< }; } -export type SpanContext = { +type SpanContext = { currentSpan: typeof currentSpan; startSpan: typeof startSpan; withCurrent: typeof withCurrent; @@ -784,15 +757,15 @@ export async function Eval< _initializeSpanContext(); // Better to return this empty object than have an annoying-to-use signature - return new EvalResultWithSummary( - { + return { + summary: { scores: {}, metrics: {}, projectName: "", experimentName: "", }, - [], - ); + results: [], + }; } const progressReporter = options.progress ?? new SimpleProgressReporter(); @@ -840,7 +813,7 @@ export async function Eval< const enableCache = options.enableCache ?? true; let ret; if (options.parent) { - ret = await withParent( + ret = await _internalWithParent( options.parent, () => runEvaluator( @@ -932,9 +905,6 @@ export async function _internalRunEvaluatorTask( ...("metadata" in datum ? datum.metadata : {}), }; const hooks: EvalHooks, EvalParameters> = { - meta(value) { - Object.assign(metadata, value); - }, metadata, expected: "expected" in datum ? datum.expected : undefined, span, @@ -1525,12 +1495,14 @@ async function runEvaluatorInternal( if (!experiment) { // This will almost always be a no-op span, but it means that if the Eval // is run in the context of a different type of span, it will be logged. + const { parent: _ignoredParent, ...spanEvent } = baseEvent; return await traced(callback, { - ...baseEvent, + ...spanEvent, state: evaluator.state, }); } else { - const result = await experiment.traced(callback, baseEvent); + const { parent: _ignoredParent, ...spanEvent } = baseEvent; + const result = await experiment.traced(callback, spanEvent); // Flush logs to provide backpressure and prevent memory accumulation // when maxConcurrency is set. Only flush when pending data exceeds the // byte threshold, avoiding excessive sequential round-trips for small @@ -1675,10 +1647,10 @@ async function runEvaluatorInternal( localScoreAccumulator ?? undefined, ); - return new EvalResultWithSummary( + return { summary, - collectResults ? collectedResults : [], - ); + results: collectResults ? collectedResults : [], + }; } finally { // Clean up disk-based span cache after eval completes and stop caching // Only if it was enabled diff --git a/js/src/framework2.ts b/js/src/framework2.ts index ab4ee52c5..ed1eef15f 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -41,7 +41,7 @@ const currentFilename = type NameOrId = { name: string } | { id: string }; -export type CreateProjectOpts = NameOrId; +type CreateProjectOpts = NameOrId; class ProjectBuilder { create(opts: CreateProjectOpts) { return new Project(opts); @@ -49,7 +49,7 @@ class ProjectBuilder { } export const projects = new ProjectBuilder(); -export class Project { +class Project { public readonly name?: string; public readonly id?: string; public tools: ToolBuilder; @@ -142,7 +142,7 @@ export class Project { } } -export class ToolBuilder { +class ToolBuilder { private taskCounter = 0; constructor(private readonly project: Project) {} @@ -212,7 +212,7 @@ export class ToolBuilder { } } -export class ScorerBuilder { +class ScorerBuilder { private taskCounter = 0; constructor(private readonly project: Project) {} @@ -335,7 +335,7 @@ type Schema = Partial<{ returns: z.ZodSchema; }>; -export type CodeOpts< +type CodeOpts< Params, Returns, Fn extends GenericFunction, @@ -376,7 +376,7 @@ type ScorerOptsUnion< | CodeOpts>, Returns, Fn> | ScorerPromptOpts; -export type ScorerOpts< +type ScorerOpts< Output, Input, Params, @@ -561,7 +561,7 @@ interface PromptNoTrace { noTrace: boolean; } -export type PromptOpts< +type PromptOpts< HasId extends boolean, HasVersion extends boolean, HasTools extends boolean = true, @@ -575,7 +575,7 @@ export type PromptOpts< (HasNoTrace extends true ? Partial : {}) & PromptDefinition; -export class PromptBuilder { +class PromptBuilder { constructor(private readonly project: Project) {} public create< diff --git a/js/src/functions/invoke.ts b/js/src/functions/invoke.ts index 82fd40de4..1cfe9a5b3 100644 --- a/js/src/functions/invoke.ts +++ b/js/src/functions/invoke.ts @@ -18,11 +18,7 @@ import { z } from "zod/v3"; /** * Arguments for the `invoke` function. */ -export interface InvokeFunctionArgs< - Input, - Output, - Stream extends boolean = false, -> { +interface InvokeFunctionArgs { // These parameters are duplicated from FunctionId, so that we can document them. /** @@ -136,7 +132,7 @@ export interface InvokeFunctionArgs< * if `stream` is true, otherwise returns the output of the function using the Zod schema's * type if present. */ -export type InvokeReturn = Stream extends true +type InvokeReturn = Stream extends true ? BraintrustStream : Output; diff --git a/js/src/functions/stream.ts b/js/src/functions/stream.ts index 392e6b337..901891119 100644 --- a/js/src/functions/stream.ts +++ b/js/src/functions/stream.ts @@ -30,41 +30,40 @@ export type BraintrustStreamChunk = | { type: "start"; data: string } | { type: "done"; data: string }; -export const braintrustStreamChunkSchema: z.ZodType = - z.union([ - z.object({ - type: z.literal("text_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("reasoning_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("json_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("error"), - data: z.string(), - }), - z.object({ - type: z.literal("console"), - data: sseConsoleEventDataSchema, - }), - z.object({ - type: z.literal("progress"), - data: sseProgressEventDataSchema, - }), - z.object({ - type: z.literal("start"), - data: z.string(), - }), - z.object({ - type: z.literal("done"), - data: z.string(), - }), - ]); +const braintrustStreamChunkSchema: z.ZodType = z.union([ + z.object({ + type: z.literal("text_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("reasoning_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("json_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("error"), + data: z.string(), + }), + z.object({ + type: z.literal("console"), + data: sseConsoleEventDataSchema, + }), + z.object({ + type: z.literal("progress"), + data: sseProgressEventDataSchema, + }), + z.object({ + type: z.literal("start"), + data: z.string(), + }), + z.object({ + type: z.literal("done"), + data: z.string(), + }), +]); /** * A Braintrust stream. This is a wrapper around a ReadableStream of `BraintrustStreamChunk`, diff --git a/js/src/graph-framework.ts b/js/src/graph-framework.ts deleted file mode 100644 index 5d6766da6..000000000 --- a/js/src/graph-framework.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { newId, Prompt } from "./logger"; -import type { - FunctionIdType as FunctionId, - GraphDataType as GraphData, - GraphNodeType as GraphNode, - GraphEdgeType as GraphEdge, - PromptBlockDataType as PromptBlockData, -} from "./generated_plain_types"; - -export interface BuildContext { - getFunctionId(functionObj: unknown): Promise; -} - -// Base interface for all node types -export interface Node { - readonly id: string; - __type: "node"; - build(context: BuildContext): Promise; - addDependency(dependency: Dependency): void; -} - -// type CallArgs = ProxyVariable | Node | Record; - -export type NodeLike = Node | Prompt | ProxyVariable; - -export type LazyGraphNode = { - type: "lazy"; - id: string; -}; - -// Graph builder class to convert functional chains to GraphData -export class GraphBuilder { - private nodes = new Map(); - private edges: Record = {}; - - private nodeLikeNodes = new Map(); // Maps node-like objects, like prompts, to their nodes - - // Special nodes - public readonly IN: InputNode; - public readonly OUT: OutputNode; - - constructor() { - // Create input and output nodes - this.IN = this.createInputNode(); - this.OUT = this.createOutputNode(); - } - - // Create the final GraphData object - public async build(context: BuildContext): Promise { - const nodes = await Promise.all( - Array.from(this.nodes.values()).map(async (node) => [ - node.id, - await node.build(context), - ]), - ); - - return { - type: "graph", - nodes: Object.fromEntries(nodes), // XXX Need to resolve the lazy nodes - edges: this.edges, - }; - } - - public addEdge({ - source, - sourceVar, - target, - targetVar, - expr, - purpose, - }: { - source: NodeLike; - sourceVar?: string; - target: NodeLike; - targetVar?: string; - expr?: string; - purpose: GraphEdge["purpose"]; - }) { - const [sourceNode, sourcePath] = this.resolveNode(source); - if (sourcePath.length > 0) { - // XXX Maybe we can remove these paths? - throw new Error("Source path must be empty"); - } - const [targetNode, targetPath] = this.resolveNode(target); - if (targetPath.length > 0) { - throw new Error("Target path must be empty"); - } - const id = this.generateId(); - - sourceVar = sourceVar ?? "output"; - targetVar = - targetVar ?? (purpose === "data" ? "input" : this.generateId("control")); - - // Make sure this variable name doesn't already exist as a target variable - for (const edge of Object.values(this.edges)) { - if ( - edge.target.node === targetNode.id && - edge.target.variable === targetVar - ) { - throw new Error( - `Variable name ${targetVar} already set on ${targetNode.id}`, - ); - } - } - - this.edges[id] = { - source: { node: sourceNode.id, variable: sourceVar }, - target: { node: targetNode.id, variable: targetVar }, - purpose, - }; - } - - public resolveNode(node: NodeLike): [Node, string[]] { - if (node instanceof Prompt) { - const cached = this.nodeLikeNodes.get(node); - if (cached) { - return [cached, []]; - } - const promptNode = this.createPromptNode(node); - this.nodeLikeNodes.set(node, promptNode); - return [promptNode, []]; - } else if (isProxyVariable(node)) { - return proxyVariableToNode(node); - } else { - return [node, []]; - } - } - - // Create a literal node - public literal(value: T): LiteralNode { - const preview = ( - typeof value === "string" ? value : JSON.stringify(value) - ).slice(0, 16); - const id = this.generateId(`literal-${preview}`); - const literalNode = new LiteralNode(this, id, value); - this.nodes.set(id, literalNode); - return literalNode; - } - - public gate(options: { condition: string }): GateNode { - const id = this.generateId("gate"); - const gateNode = new GateNode(this, id, options.condition); - this.nodes.set(id, gateNode); - return gateNode; - } - - public aggregator(): AggregatorNode { - const id = this.generateId("aggregator"); - const aggregatorNode = new AggregatorNode(this, id); - this.nodes.set(id, aggregatorNode); - return aggregatorNode; - } - - public promptTemplate(options: { - prompt: PromptBlockData; - }): PromptTemplateNode { - const id = this.generateId("prompt-template"); - const promptTemplateNode = new PromptTemplateNode(this, id, options.prompt); - this.nodes.set(id, promptTemplateNode); - return promptTemplateNode; - } - - // public call(node: NodeLike, input: CallArgs): Node { - // const [resolvedNode, path] = this.resolveNode(node); - // if (resolvedNode instanceof SingleInputNode) { - // return resolvedNode.call(input, path); - // } else { - // throw new Error("Node must be a SingleInputNode"); - // } - // } - - // Helper to generate node IDs - private generateId(name?: string): string { - const uuid = newId(); - if (name) { - return `${name}-${uuid.slice(0, 8)}`; - } else { - return uuid; - } - } - - // Create an input node - private createInputNode(): InputNode { - const id = this.generateId("input"); - const inputNode = new InputNode(this, id); - this.nodes.set(id, inputNode); - return inputNode; - } - - // Create an output node - private createOutputNode(): OutputNode { - const id = this.generateId("output"); - const outputNode = new OutputNode(this, id); - this.nodes.set(id, outputNode); - return outputNode; - } - - // Create a prompt node from a CodePrompt - private createPromptNode(prompt: Prompt): PromptNode { - const id = this.generateId(`prompt-${prompt.slug}`); - - const promptNode = new PromptNode(this, id, prompt); - this.nodes.set(id, promptNode); - return promptNode; - } -} - -export type ProxyVariable = { - [key: string]: ProxyVariable; -}; - -function isProxyVariable(node: unknown): node is ProxyVariable { - return ( - typeof node === "object" && - node !== null && - "__type" in node && - // @ts-ignore - node.__type === "proxy-variable" - ); -} - -function proxyVariableToNode(proxy: ProxyVariable): [Node, string[]] { - // @ts-ignore - return [proxy.__node, proxy.__path]; -} - -// // Create a proxy handler that captures property access paths -// function createVariableProxy({ -// path, -// node, -// }: { -// path: string[]; -// node: Node; -// }): ProxyVariable { -// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -// return new Proxy({} as ProxyVariable, { -// get(target, prop) { -// if (typeof prop === "string") { -// if (prop === "__type") { -// return "proxy-variable"; -// } else if (prop === "__node") { -// return node; -// } else if (prop === "__path") { -// return path; -// } - -// const newPath = [...path, prop]; - -// // Return a variable reference for terminal properties -// // or a new proxy for further chaining -// return createVariableProxy({ path: newPath, node }); -// } -// return undefined; -// }, -// has(target, prop) { -// return typeof prop === "string"; -// }, -// }); -// } - -// Type for transform functions -export type TransformFn = (input: ProxyVariable) => Node; - -interface Dependency { - node: Node; - sourceVar?: string; - targetVar?: string; - expr?: string; -} - -// Base Node class for common functionality -abstract class BaseNode implements Node { - public readonly __type = "node"; - public dependencies: Dependency[] = []; - - constructor( - protected graph: GraphBuilder, - public readonly id: string, - ) {} - - public addDependency(dependency: Dependency) { - this.dependencies.push(dependency); - } - - abstract build(context: BuildContext): Promise; -} - -// abstract class SingleOutputNode extends BaseNode { -// // Connect this node to another node -// public then(...args: Array): Node { -// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -// const callableThis = this as unknown as Node; -// let lastNode: Node = callableThis; - -// // Connect each arg to this node -// for (const arg of args) { -// // Handle different types of arguments -// if (typeof arg === "function") { -// // This function is expected to take dependencies as needed on the argsProxy. -// const argsProxy = createVariableProxy({ path: [], node: callableThis }); -// const result = arg(argsProxy); -// lastNode = result; -// } else { -// const [node, path] = this.graph.resolveNode(arg); -// lastNode = node; -// node.addDependency({ node: callableThis, expr: escapePath(path) }); -// } -// } - -// return lastNode; -// } -// } - -// abstract class SingleInputNode extends BaseNode { -// public call(input: CallArgs, path?: string[]): Node { -// if (isProxyVariable(input)) { -// const [sourceNode, sourcePath] = proxyVariableToNode(input); -// this.addDependency({ node: sourceNode, expr: escapePath(sourcePath) }); -// } else if (isNode(input)) { -// this.addDependency({ -// node: input, -// expr: path ? escapePath(path) : undefined, -// }); -// } else { -// for (const [targetVar, source] of Object.entries(input)) { -// const [sourceNode, sourcePath] = this.graph.resolveNode(source); -// this.addDependency({ -// node: sourceNode, -// expr: sourcePath ? escapePath(sourcePath) : undefined, -// targetVar, -// }); -// } -// } -// return this; -// } -// } - -// function isNode(node: unknown): node is Node { -// return ( -// typeof node === "object" && -// node !== null && -// "__type" in node && -// // @ts-ignore -// node.__type === "node" -// ); -// } - -// Input node (entry point to the graph) -export class InputNode extends BaseNode implements Node { - constructor(graph: GraphBuilder, id: string) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "input", - description: "Input to the graph", - }; - } -} - -// Output node (exit point from the graph) -export class OutputNode extends BaseNode implements Node { - constructor(graph: GraphBuilder, id: string) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "output", - description: "Output of the graph", - }; - } -} - -// Prompt node (wrapper for CodePrompt) -export class PromptNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private prompt: Prompt, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "function", - function: await context.getFunctionId(this.prompt), - }; - } -} - -// Gate node for conditional branching -export class GateNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private condition: string, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "gate", - description: "Conditional gate", - condition: this.condition, - }; - } -} - -export class AggregatorNode extends BaseNode implements Node { - constructor(graph: GraphBuilder, id: string) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "aggregator", - description: "Aggregator", - }; - } -} - -export class PromptTemplateNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private prompt: PromptBlockData, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "prompt_template", - prompt: this.prompt, - }; - } -} - -// Literal node for constant values -export class LiteralNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private value: T, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "literal", - value: this.value, - }; - } -} - -// Create a graph instance with IN and OUT nodes -export function createGraph(): GraphBuilder { - const graphBuilder = new GraphBuilder(); - return graphBuilder; -} - -// XXX write tests -export function escapePath(parts: string[]): string | undefined { - if (parts.length === 0) { - return undefined; - } - return parts - .map((part) => { - if (/[^\w-]/.test(part)) { - // Escape special characters properly - const escaped = part.replace(/["\\]/g, "\\$&"); - return `"${escaped}"`; - } - return part; - }) - .join("."); -} - -export function unescapePath(path: string): string[] { - const regex = /"((?:\\["\\]|[^"\\])*)"|([^\.]+)/g; - const matches = path.match(regex); - return matches - ? matches.map((match) => { - if (match.startsWith('"')) { - // Remove surrounding quotes and unescape special characters - return match.slice(1, -1).replace(/\\(["\\])/g, "$1"); - } - return match; - }) - : []; -} - -// Export the graph constructor -export default { createGraph }; diff --git a/js/src/id-gen.ts b/js/src/id-gen.ts index 67925916a..d95e1a0bc 100644 --- a/js/src/id-gen.ts +++ b/js/src/id-gen.ts @@ -136,15 +136,10 @@ export function resolveUseLegacyUuidIds(): boolean { * This eliminates global state and makes tests parallelizable. * Each caller gets their own generator instance. * - * Honors an explicitly-installed `globalThis.BRAINTRUST_ID_GENERATOR` (e.g. set - * by `@braintrust/otel`'s `setupOtelCompat()`). Otherwise it defaults to - * OpenTelemetry-compatible hex IDs, falling back to legacy UUID IDs when - * BRAINTRUST_LEGACY_IDS is set. + * Defaults to OpenTelemetry-compatible hex IDs, falling back to legacy UUID + * IDs when BRAINTRUST_LEGACY_IDS is set. */ export function getIdGenerator(): IDGenerator { - if (globalThis.BRAINTRUST_ID_GENERATOR !== undefined) { - return new globalThis.BRAINTRUST_ID_GENERATOR(); - } return resolveUseLegacyUuidIds() ? new UUIDGenerator() : new OTELIDGenerator(); diff --git a/js/src/instrumentation/config.ts b/js/src/instrumentation/config.ts index e8df49037..edcbe083e 100644 --- a/js/src/instrumentation/config.ts +++ b/js/src/instrumentation/config.ts @@ -1,16 +1,13 @@ export interface InstrumentationIntegrationsConfig { openai?: boolean; anthropic?: boolean; - vercel?: boolean; aisdk?: boolean; - google?: boolean; googleGenAI?: boolean; googleADK?: boolean; huggingface?: boolean; claudeAgentSDK?: boolean; cloudflareAIChat?: boolean; cloudflareThink?: boolean; - cursor?: boolean; cursorSDK?: boolean; mastra?: boolean; openAIAgents?: boolean; @@ -20,8 +17,6 @@ export interface InstrumentationIntegrationsConfig { ollama?: boolean; cohere?: boolean; groq?: boolean; - bedrock?: boolean; - awsBedrock?: boolean; awsBedrockRuntime?: boolean; genkit?: boolean; gitHubCopilot?: boolean; @@ -69,14 +64,14 @@ const envIntegrationAliases: Record< aisdk: "aisdk", "ai-sdk": "aisdk", "vercel-ai": "aisdk", - vercel: "vercel", + vercel: "aisdk", claudeagentsdk: "claudeAgentSDK", "claude-agent-sdk": "claudeAgentSDK", cloudflareaichat: "cloudflareAIChat", "cloudflare-ai-chat": "cloudflareAIChat", "@cloudflare/ai-chat": "cloudflareAIChat", cloudflarethink: "cloudflareThink", - cursor: "cursor", + cursor: "cursorSDK", "cursor-sdk": "cursorSDK", cursorsdk: "cursorSDK", mastra: "mastra", @@ -84,7 +79,7 @@ const envIntegrationAliases: Record< openaiagents: "openAIAgents", "openai-agents-core": "openAIAgents", openaiagentscore: "openAIAgents", - google: "google", + google: "googleGenAI", "google-genai": "googleGenAI", googlegenai: "googleGenAI", huggingface: "huggingface", @@ -100,9 +95,9 @@ const envIntegrationAliases: Record< cohere: "cohere", groq: "groq", "groq-sdk": "groq", - bedrock: "bedrock", - "aws-bedrock": "awsBedrock", - awsbedrock: "awsBedrock", + bedrock: "awsBedrockRuntime", + "aws-bedrock": "awsBedrockRuntime", + awsbedrock: "awsBedrockRuntime", "aws-bedrock-runtime": "awsBedrockRuntime", awsbedrockruntime: "awsBedrockRuntime", "@aws-sdk/client-bedrock-runtime": "awsBedrockRuntime", @@ -129,16 +124,13 @@ export function getDefaultInstrumentationIntegrations(): Record< openai: true, openaiCodexSDK: true, anthropic: true, - vercel: true, aisdk: true, - google: true, googleGenAI: true, googleADK: true, huggingface: true, claudeAgentSDK: true, cloudflareAIChat: true, cloudflareThink: true, - cursor: true, cursorSDK: true, mastra: true, openAIAgents: true, @@ -148,8 +140,6 @@ export function getDefaultInstrumentationIntegrations(): Record< ollama: true, cohere: true, groq: true, - bedrock: true, - awsBedrock: true, awsBedrockRuntime: true, genkit: true, gitHubCopilot: true, diff --git a/js/src/instrumentation/core/channel-tracing.ts b/js/src/instrumentation/core/channel-tracing.ts index 1f3ff47d0..7d69fb12b 100644 --- a/js/src/instrumentation/core/channel-tracing.ts +++ b/js/src/instrumentation/core/channel-tracing.ts @@ -1,11 +1,7 @@ import { debugLogger } from "../../debug-logger"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; -import { - _internalGetGlobalState, - BRAINTRUST_CURRENT_SPAN_STORE, - startSpan, -} from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +import { _internalGetGlobalState, startSpan } from "../../logger"; +import type { Span } from "../../logger"; import { withSpanInstrumentationName, type SpanInstrumentationName, @@ -316,13 +312,7 @@ function bindCurrentSpanStoreToStart< const state = _internalGetGlobalState(); const startChannel = tracingChannel.start; const contextManager = state?.contextManager; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!currentSpanStore || !startChannel) { return; diff --git a/js/src/instrumentation/index.test.ts b/js/src/instrumentation/index.test.ts index fd5442745..b51519344 100644 --- a/js/src/instrumentation/index.test.ts +++ b/js/src/instrumentation/index.test.ts @@ -8,6 +8,7 @@ describe("instrumentation public API", () => { "braintrustEveInstrumentation", "braintrustFlueInstrumentation", "configureInstrumentation", + "registerOtelFlush", ]); }); }); diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index e148f2dc5..67853cce8 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -15,3 +15,6 @@ export { braintrustEveInstrumentation } from "./providers/eve-instrumentation"; // Configuration API export { configureInstrumentation } from "./registry"; export type { InstrumentationConfig } from "./registry"; + +// Integration hook used by @braintrust/otel so core flushes also drain OTEL. +export { registerOtelFlush } from "../logger"; diff --git a/js/src/instrumentation/instrumentation-consumers.test.ts b/js/src/instrumentation/instrumentation-consumers.test.ts index ce4837bb5..9499bff31 100644 --- a/js/src/instrumentation/instrumentation-consumers.test.ts +++ b/js/src/instrumentation/instrumentation-consumers.test.ts @@ -128,7 +128,7 @@ const integrationCases: Array< > = [ ["aisdk", [registrations.aiSDK]], ["anthropic", [registrations.anthropic]], - ["bedrock", [registrations.bedrockRuntime]], + ["awsBedrockRuntime", [registrations.bedrockRuntime]], ["claudeAgentSDK", [registrations.claudeAgentSDK]], ["cloudflareAgents", [registrations.cloudflareAgents]], ["cloudflareAIChat", [registrations.cloudflareAIChat]], @@ -187,15 +187,8 @@ describe("registerInstrumentationConsumers", () => { }, ); - it.each([ - ["vercel", registrations.aiSDK], - ["google", registrations.googleGenAI], - ["cursor", registrations.cursorSDK], - ["awsBedrock", registrations.bedrockRuntime], - ["awsBedrockRuntime", registrations.bedrockRuntime], - ["langgraph", registrations.langChain], - ] as const)( - "honors the legacy or related %s disable flag", + it.each([["langgraph", registrations.langChain]] as const)( + "honors the related %s disable flag", (integration, register) => { registerInstrumentationConsumers({ integrations: { [integration]: false }, diff --git a/js/src/instrumentation/instrumentation-consumers.ts b/js/src/instrumentation/instrumentation-consumers.ts index ed32522a9..57d01b43d 100644 --- a/js/src/instrumentation/instrumentation-consumers.ts +++ b/js/src/instrumentation/instrumentation-consumers.ts @@ -46,8 +46,7 @@ export function registerInstrumentationConsumers( registerAnthropicInstrumentation(); } - // Support both 'aisdk' and legacy 'vercel' config keys - if (integrations.aisdk !== false && integrations.vercel !== false) { + if (integrations.aisdk !== false) { registerAISDKInstrumentation(); } @@ -59,7 +58,7 @@ export function registerInstrumentationConsumers( registerCloudflareThinkInstrumentation(); } - if (integrations.cursorSDK !== false && integrations.cursor !== false) { + if (integrations.cursorSDK !== false) { registerCursorSDKInstrumentation(); } @@ -67,8 +66,7 @@ export function registerInstrumentationConsumers( registerOpenAIAgentsInstrumentation(); } - // Support both 'googleGenAI' and legacy 'google' config keys - if (integrations.googleGenAI !== false && integrations.google !== false) { + if (integrations.googleGenAI !== false) { registerGoogleGenAIInstrumentation(); } @@ -109,11 +107,7 @@ export function registerInstrumentationConsumers( registerGroqInstrumentation(); } - if ( - integrations.bedrock !== false && - integrations.awsBedrock !== false && - integrations.awsBedrockRuntime !== false - ) { + if (integrations.awsBedrockRuntime !== false) { registerBedrockRuntimeInstrumentation(); } diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts index 93265e4a3..1394ac919 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts @@ -32,18 +32,22 @@ vi.mock("../core/stream-patcher", () => ({ import { registerClaudeAgentSDKInstrumentation } from "./claude-agent-sdk-instrumentation"; import iso from "../../isomorph"; -import { startSpan } from "../../logger"; +import { _internalStartSpan as startSpan } from "../../logger"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; // Mock the logger module -vi.mock("../../logger", () => ({ - startSpan: vi.fn(() => ({ +vi.mock("../../logger", () => { + const startSpan = vi.fn(() => ({ log: vi.fn(), end: vi.fn(), export: vi.fn(() => Promise.resolve({})), - })), -})); + })); + return { + startSpan, + _internalStartSpan: startSpan, + }; +}); // Mock utility modules vi.mock("../../../util/index", () => ({ diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts index 9944ebe60..32b5ab345 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts @@ -1,7 +1,7 @@ import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers } from "../../isomorph"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts index 1a1c7db88..2ba99e8c9 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts @@ -1,4 +1,4 @@ -import { startSpan as startBaseSpan, withCurrent } from "../../logger"; +import { _internalStartSpan as startBaseSpan, withCurrent } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts index 860ab0b18..f0a0c3a72 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts @@ -1,25 +1,19 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const { - mockInternalGetGlobalState, - mockStartSpan, - mockWithCurrent, - mockCurrentSpanStoreSymbol, -} = vi.hoisted(() => ({ - mockCurrentSpanStoreSymbol: Symbol.for("braintrust.currentSpanStore"), - mockInternalGetGlobalState: vi.fn(() => undefined), - mockStartSpan: vi.fn(), - mockWithCurrent: vi.fn((_span: unknown, callback: () => unknown) => - callback(), - ), -})); +const { mockInternalGetGlobalState, mockStartSpan, mockWithCurrent } = + vi.hoisted(() => ({ + mockInternalGetGlobalState: vi.fn(() => undefined), + mockStartSpan: vi.fn(), + mockWithCurrent: vi.fn((_span: unknown, callback: () => unknown) => + callback(), + ), + })); vi.mock("../../isomorph", () => ({ default: { newTracingChannel: vi.fn() }, })); vi.mock("../../logger", () => ({ - BRAINTRUST_CURRENT_SPAN_STORE: mockCurrentSpanStoreSymbol, _internalGetGlobalState: () => mockInternalGetGlobalState(), startSpan: (...args: unknown[]) => (mockStartSpan as any)(...args), withCurrent: (...args: unknown[]) => (mockWithCurrent as any)(...args), diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts index a75ad3ea6..2258ea666 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts @@ -1,12 +1,11 @@ import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { - BRAINTRUST_CURRENT_SPAN_STORE, _internalGetGlobalState, startSpan as startBaseSpan, withCurrent, } from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +import type { Span } from "../../logger"; import { debugLogger } from "../../debug-logger"; import { INSTRUMENTATION_NAMES, @@ -134,13 +133,7 @@ class CloudflareAIChatInstrumentationConsumer { const globalState = _internalGetGlobalState(); const contextManager = globalState?.contextManager; const startChannel = tracingChannel.start; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel || !currentSpanStore || !contextManager) { return; diff --git a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts index 66196becf..17751542a 100644 --- a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts @@ -1,11 +1,7 @@ import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; -import { - BRAINTRUST_CURRENT_SPAN_STORE, - _internalGetGlobalState, - startSpan, -} from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +import { _internalGetGlobalState, startSpan } from "../../logger"; +import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -63,13 +59,7 @@ class CloudflareThinkInstrumentationConsumer { const states = new WeakMap(); const state = _internalGetGlobalState(); const contextManager = state?.contextManager; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); const ensureState = ( event: ChannelMessage, diff --git a/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts index c30169c35..39d3582a6 100644 --- a/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts @@ -12,6 +12,7 @@ vi.mock("../../isomorph", () => ({ vi.mock("../../logger", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); import iso from "../../isomorph"; diff --git a/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts index 41118be17..957d8a997 100644 --- a/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts @@ -2,7 +2,7 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/eve-instrumentation.test.ts b/js/src/instrumentation/providers/eve-instrumentation.test.ts index 9e1a6bc28..48e6b5bff 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.test.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.test.ts @@ -93,9 +93,9 @@ describe("braintrustEveInstrumentation", () => { ]); }); - it("exports only the Eve instrumentation provider API", () => { - expect(braintrustExports.braintrustEveInstrumentation).toBe( - braintrustEveInstrumentation, + it("exports the Eve provider only from the instrumentation entrypoint", () => { + expect(braintrustExports).not.toHaveProperty( + "braintrustEveInstrumentation", ); expect(instrumentationExports.braintrustEveInstrumentation).toBe( braintrustEveInstrumentation, diff --git a/js/src/instrumentation/providers/flue-instrumentation.test.ts b/js/src/instrumentation/providers/flue-instrumentation.test.ts index ae6fbec8b..f1c05a12c 100644 --- a/js/src/instrumentation/providers/flue-instrumentation.test.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.test.ts @@ -3,17 +3,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { mockDebugLog, mockCurrentParentSpan, - mockCurrentSpanStoreSymbol, mockCurrentSpanStore, mockFlush, mockStartSpan, } = vi.hoisted(() => { const currentParentSpan = { current: undefined as any }; - const currentSpanStoreSymbol = Symbol.for("braintrust.currentSpanStore"); return { mockDebugLog: vi.fn(), mockCurrentParentSpan: currentParentSpan, - mockCurrentSpanStoreSymbol: currentSpanStoreSymbol, mockCurrentSpanStore: { getStore: vi.fn(() => currentParentSpan.current), run: vi.fn((span: unknown, callback: () => unknown) => { @@ -38,12 +35,11 @@ vi.mock("../../debug-logger", () => ({ })); vi.mock("../../logger", () => ({ - BRAINTRUST_CURRENT_SPAN_STORE: mockCurrentSpanStoreSymbol, NOOP_SPAN: {}, flush: (...args: unknown[]) => mockFlush(...args), _internalGetGlobalState: () => ({ contextManager: { - [mockCurrentSpanStoreSymbol]: mockCurrentSpanStore, + getCurrentSpanStore: () => mockCurrentSpanStore, wrapSpanForStore: (span: unknown) => span, }, idGenerator: { @@ -53,6 +49,7 @@ vi.mock("../../logger", () => ({ }, }), startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), withCurrent: (span: unknown, callback: () => unknown) => { const previous = mockCurrentParentSpan.current; mockCurrentParentSpan.current = span; diff --git a/js/src/instrumentation/providers/flue-instrumentation.ts b/js/src/instrumentation/providers/flue-instrumentation.ts index 1426afde6..51fa32881 100644 --- a/js/src/instrumentation/providers/flue-instrumentation.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.ts @@ -1,11 +1,10 @@ import { toLoggedError } from "../core"; import { debugLogger } from "../../debug-logger"; import { - BRAINTRUST_CURRENT_SPAN_STORE, NOOP_SPAN, flush, _internalGetGlobalState, - startSpan as startBaseSpan, + _internalStartSpan as startBaseSpan, withCurrent, } from "../../logger"; import type { Span, StartSpanArgs } from "../../logger"; @@ -1308,9 +1307,7 @@ function runWithCurrentSpanStore( ): Promise { const state = _internalGetGlobalState(); const contextManager = state?.contextManager; - const currentSpanStore = contextManager - ? Reflect.get(contextManager, BRAINTRUST_CURRENT_SPAN_STORE) - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (contextManager && typeof currentSpanStore?.run === "function") { return currentSpanStore.run(contextManager.wrapSpanForStore(span), next); diff --git a/js/src/instrumentation/providers/genkit-instrumentation.ts b/js/src/instrumentation/providers/genkit-instrumentation.ts index e485295ea..16abe033c 100644 --- a/js/src/instrumentation/providers/genkit-instrumentation.ts +++ b/js/src/instrumentation/providers/genkit-instrumentation.ts @@ -8,7 +8,6 @@ import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { _internalGetGlobalState, - BRAINTRUST_CURRENT_SPAN_STORE, startSpan as startBaseSpan, } from "../../logger"; import type { CurrentSpanStore, Span } from "../../logger"; @@ -291,13 +290,7 @@ function bindActionCurrentSpanStoreToStart< ) => void; } & object) | undefined; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel?.bindStore || !currentSpanStore) { return; diff --git a/js/src/instrumentation/providers/github-copilot-instrumentation.ts b/js/src/instrumentation/providers/github-copilot-instrumentation.ts index b5f14976f..bf53ebd72 100644 --- a/js/src/instrumentation/providers/github-copilot-instrumentation.ts +++ b/js/src/instrumentation/providers/github-copilot-instrumentation.ts @@ -1,5 +1,5 @@ import type { IsoChannelHandlers } from "../../isomorph"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/google-adk-instrumentation.test.ts b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts index f4efcfc7d..12c8a9175 100644 --- a/js/src/instrumentation/providers/google-adk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -const { - mockCurrentSpanStoreSymbol: MOCK_CURRENT_SPAN_STORE_SYMBOL, - mockInternalGetGlobalState, -} = vi.hoisted(() => ({ - mockCurrentSpanStoreSymbol: Symbol.for("braintrust.currentSpanStore"), +const { mockInternalGetGlobalState } = vi.hoisted(() => ({ mockInternalGetGlobalState: vi.fn(() => undefined), })); @@ -31,7 +27,6 @@ vi.mock("../../logger", () => ({ startSpan: (...args: any[]) => (mockStartSpan as any)(...args), _internalGetGlobalState: (...args: any[]) => (mockInternalGetGlobalState as any)(...args), - BRAINTRUST_CURRENT_SPAN_STORE: MOCK_CURRENT_SPAN_STORE_SYMBOL, withCurrent: (_span: any, callback: () => unknown) => callback(), Attachment: class MockAttachment { reference: any; @@ -183,7 +178,7 @@ describe("registerGoogleADKInstrumentation", () => { const wrapSpanForStore = vi.fn(() => "wrapped-runner-store"); mockInternalGetGlobalState.mockReturnValue({ contextManager: { - [MOCK_CURRENT_SPAN_STORE_SYMBOL]: currentSpanStore, + getCurrentSpanStore: () => currentSpanStore, wrapSpanForStore, }, } as any); @@ -492,7 +487,7 @@ describe("registerGoogleADKInstrumentation", () => { const wrapSpanForStore = vi.fn(() => "wrapped-agent-store"); mockInternalGetGlobalState.mockReturnValue({ contextManager: { - [MOCK_CURRENT_SPAN_STORE_SYMBOL]: currentSpanStore, + getCurrentSpanStore: () => currentSpanStore, wrapSpanForStore, }, } as any); diff --git a/js/src/instrumentation/providers/google-adk-instrumentation.ts b/js/src/instrumentation/providers/google-adk-instrumentation.ts index 994779305..de631de88 100644 --- a/js/src/instrumentation/providers/google-adk-instrumentation.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.ts @@ -2,7 +2,6 @@ import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { - BRAINTRUST_CURRENT_SPAN_STORE, _internalGetGlobalState, startSpan as startBaseSpan, withCurrent, @@ -518,13 +517,7 @@ function bindCurrentSpanStoreToStart< ) => void; } & object) | undefined; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel?.bindStore || !currentSpanStore) { return; diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.ts b/js/src/instrumentation/providers/google-genai-instrumentation.ts index 85e5ae049..bd3029147 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.ts @@ -8,7 +8,6 @@ import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { _internalGetGlobalState, Attachment, - BRAINTRUST_CURRENT_SPAN_STORE, startSpan as startBaseSpan, type CurrentSpanStore, type Span, @@ -356,13 +355,7 @@ function bindCurrentSpanStoreToStart< ) => void; } & object) | undefined; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel?.bindStore || !currentSpanStore) { return; diff --git a/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts index f7250bb3b..c919b6d3b 100644 --- a/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts @@ -12,6 +12,7 @@ vi.mock("../../isomorph", () => ({ vi.mock("../../logger", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); import iso from "../../isomorph"; diff --git a/js/src/instrumentation/providers/openai-codex-instrumentation.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.ts index abf442120..eec4a36a2 100644 --- a/js/src/instrumentation/providers/openai-codex-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.ts @@ -2,8 +2,8 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; -import type { Span, StartSpanArgs } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; +import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -27,6 +27,8 @@ import type { OpenAICodexWebSearchItem, } from "../../vendor-sdk-types/openai-codex"; +type InternalStartSpanArgs = NonNullable[0]>; + type CodexRunState = { activeLlmSpan?: CodexLlmSpanState; activeItemSpans: Map; @@ -565,7 +567,7 @@ async function itemSpanArgs( item: OpenAICodexThreadItem, ): Promise< | { - start: StartSpanArgs; + start: InternalStartSpanArgs; end: Parameters[0]; } | undefined diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts index 7e7d406ed..48b38ea1f 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts @@ -27,6 +27,7 @@ vi.mock("../../isomorph", async (importOriginal) => { vi.mock("../../logger", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts index 11829e004..22537472b 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts @@ -2,7 +2,7 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import iso, { type IsoAsyncLocalStorage } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 010aee634..60520f46c 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -135,7 +135,7 @@ export const registry = new InstrumentationRegistry(); * * @example * ```typescript - * import { configureInstrumentation } from 'braintrust'; + * import { configureInstrumentation } from 'braintrust/instrumentation'; * * // Disable OpenAI instrumentation * configureInstrumentation({ diff --git a/js/src/logger-misc.test.ts b/js/src/logger-misc.test.ts index bd7051544..730bacb68 100644 --- a/js/src/logger-misc.test.ts +++ b/js/src/logger-misc.test.ts @@ -10,7 +10,7 @@ import { BaseAttachment, Attachment, ExternalAttachment, - initExperiment, + init, initLogger, NOOP_SPAN, permalink, @@ -368,7 +368,7 @@ describe("span.link", () => { expect(state.appUrl).toBeDefined(); // Create a test experiment - const experiment = initExperiment("test-experiment"); + const experiment = init({ project: "test-experiment" }); // Get a span within the experiment context const span = experiment.startSpan({ @@ -417,7 +417,7 @@ describe("span.link", () => { test("span.link handles missing experiment id", async () => { await _exportsForTestingOnly.simulateLoginForTests(); - const experiment = initExperiment("test-experiment"); + const experiment = init({ project: "test-experiment" }); const span = experiment.startSpan({ name: "test-span" }); span.end(); // Force parentObjectId to be undefined diff --git a/js/src/logger.test.ts b/js/src/logger.test.ts index 1dffdbb49..6b67268fd 100644 --- a/js/src/logger.test.ts +++ b/js/src/logger.test.ts @@ -11,9 +11,11 @@ import { } from "vitest"; import { _exportsForTestingOnly, + _internalStartSpan, init, initDataset, initLogger, + extractTraceContextFromHeaders, Prompt, BraintrustState, loadPrompt, @@ -898,7 +900,8 @@ test("legacy initDataset applies bt eval internal BTQL runtime value", async () }, }); - const dataset = initDataset("test-project", { + const dataset = initDataset({ + project: "test-project", dataset: "test-dataset", state, }); @@ -2997,7 +3000,7 @@ describe("parent precedence", () => { test("withParent + wrapTraced: child spans attach to current span (not directly to withParent)", async () => { const logger = initLogger({ projectName: "test", projectId: "pid" }); const outer = logger.startSpan({ name: "outer" }); - const parentStr = await outer.export(); + const parentContext = extractTraceContextFromHeaders(outer.inject())!; outer.end(); const inner = wrapTraced( @@ -3007,7 +3010,7 @@ describe("parent precedence", () => { { name: "inner" }, ); - await withParent(parentStr, () => inner()); + await withParent(parentContext, () => inner()); await memory.flush(); const events = await memory.drain(); @@ -3051,7 +3054,7 @@ describe("parent precedence", () => { const inner = wrapTraced( async function inner() { - startSpan({ name: "forced", parent: parentStr }).end(); + _internalStartSpan({ name: "forced", parent: parentStr }).end(); }, { name: "inner" }, ); @@ -3083,7 +3086,7 @@ describe("parent precedence", () => { const child = secondaryLogger.startSpan({ name: "child", - parent: parentStr, + parent: parentStr as never, }); child.end(); @@ -3114,7 +3117,7 @@ describe("parent precedence", () => { const child = secondaryExperiment.startSpan({ name: "child", - parent: parentStr, + parent: parentStr as never, }); child.end(); diff --git a/js/src/logger.ts b/js/src/logger.ts index 793cb96fc..62000ba17 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -37,8 +37,7 @@ import { batchItems, constructJsonArray, DatasetRecord, - DEFAULT_IS_LEGACY_DATASET, - ensureDatasetRecord, + ensureNewDatasetRecord, ExperimentEvent, ExperimentLogFullArgs, ExperimentLogPartialArgs, @@ -313,17 +312,20 @@ export type StartSpanArgs = { spanAttributes?: Record; startTime?: number; /** - * The parent to start this span under. May be an exported span slug string - * (from `span.export()`) or an opaque W3C trace-context (from - * {@link extractTraceContextFromHeaders}). + * An opaque W3C trace context returned by + * {@link extractTraceContextFromHeaders}. */ - parent?: string | PropagationContext; + parent?: PropagationContext; event?: StartSpanEventArgs; propagatedEvent?: StartSpanEventArgs; spanId?: string; parentSpanIds?: ParentSpanIds | MultiParentSpanIds; }; +type InternalStartSpanArgs = Omit & { + parent?: string | PropagationContext; +}; + export type EndSpanArgs = { endTime?: number; }; @@ -431,8 +433,8 @@ export interface Span extends Exportable { * parameters of {@link Span.startSpan} for usage details. * * Callers should treat the return value as opaque. The serialization format - * may change from time to time. If parsing is needed, use - * `SpanComponentsV3.fromStr`. + * may change from time to time. For cross-service propagation, prefer + * {@link Span.inject} and {@link extractTraceContextFromHeaders}. * * @returns Serialized representation of this span's identifiers. */ @@ -489,11 +491,6 @@ export interface Span extends Exportable { */ flush(): Promise; - /** - * Alias for `end`. - */ - close(args?: EndSpanArgs): number; - /** * Set the span's name, type, or other attributes after it's created. */ @@ -518,12 +515,8 @@ export interface Span extends Exportable { kind: "span"; } -export const BRAINTRUST_CURRENT_SPAN_STORE = Symbol.for( - "braintrust.currentSpanStore", -); - /** - * The type of AsyncLocalStorage exposed via {@link BRAINTRUST_CURRENT_SPAN_STORE}. + * The AsyncLocalStorage contract used by context-manager integrations. * * The stored value is intentionally opaque (`unknown`) because the concrete type * depends on the active context manager: @@ -540,6 +533,14 @@ export abstract class ContextManager { abstract runInContext(span: Span, callback: () => R): R; abstract getCurrentSpan(): Span | undefined; + /** + * Return the store used by instrumentation hooks to propagate the current + * span, when the context manager supports direct store binding. + */ + getCurrentSpanStore(): CurrentSpanStore | undefined { + return undefined; + } + /** * Returns the value to store in the ALS bound to a global hook's start event. * In default mode this is the Span itself; in OTEL mode it is the OTEL Context @@ -553,12 +554,14 @@ export abstract class ContextManager { class BraintrustContextManager extends ContextManager { private _currentSpan: IsoAsyncLocalStorage; - [BRAINTRUST_CURRENT_SPAN_STORE]: IsoAsyncLocalStorage; constructor() { super(); this._currentSpan = iso.newAsyncLocalStorage(); - this[BRAINTRUST_CURRENT_SPAN_STORE] = this._currentSpan; + } + + getCurrentSpanStore(): CurrentSpanStore { + return this._currentSpan; } getParentSpanIds(): ContextParentSpanIds | undefined { @@ -582,11 +585,8 @@ class BraintrustContextManager extends ContextManager { } } -// make sure to update @braintrust/otel package declare global { var BRAINTRUST_CONTEXT_MANAGER: (new () => ContextManager) | undefined; - var BRAINTRUST_ID_GENERATOR: (new () => IDGenerator) | undefined; - var BRAINTRUST_SPAN_COMPONENT: SpanComponent | undefined; } type SpanComponent = typeof SpanComponentsV3 | typeof SpanComponentsV4; @@ -597,11 +597,7 @@ type SpanComponent = typeof SpanComponentsV3 | typeof SpanComponentsV4; // serialize as V4, legacy UUID IDs serialize as V3. These must move together -- // serializing hex IDs via V3 would lose the compact encoding and risk // corrupting hex values that happen to parse as UUIDs. An explicit -// `globalThis.BRAINTRUST_SPAN_COMPONENT` (e.g. from `@braintrust/otel`) wins. function getSpanComponentsClass(): SpanComponent { - if (globalThis.BRAINTRUST_SPAN_COMPONENT) { - return globalThis.BRAINTRUST_SPAN_COMPONENT; - } return resolveUseLegacyUuidIds() ? SpanComponentsV3 : SpanComponentsV4; } @@ -611,6 +607,20 @@ export function getContextManager(): ContextManager { : new BraintrustContextManager(); } +/** + * Configure the context manager used by Braintrust. + * + * This extension point is intended for runtime integrations such as + * `@braintrust/otel` and `@braintrust/browser`. Passing `undefined` restores + * Braintrust's default context manager. + */ +export function configureContextManager( + contextManager: (new () => ContextManager) | undefined, +): void { + globalThis.BRAINTRUST_CONTEXT_MANAGER = contextManager; + _globalState?.[RESET_CONTEXT_MANAGER_STATE](); +} + /** * A fake implementation of the Span API which does nothing. This can be used as the default span. */ @@ -674,10 +684,6 @@ export class NoopSpan implements Span { public async flush(): Promise {} - public close(args?: EndSpanArgs): number { - return this.end(args); - } - public setAttributes(_args: Omit) {} public startSpanWithParents( @@ -3851,38 +3857,7 @@ type InitializedExperiment = */ export function init( options: Readonly>, -): InitializedExperiment; - -/** - * Legacy form of `init` which accepts the project name as the first parameter, - * separately from the remaining options. See `init(options)` for full details. - */ -export function init( - project: string, - options?: Readonly>, -): InitializedExperiment; - -/** - * Combined overload implementation of `init`. Do not call this directly. - * Instead, call `init(options)` or `init(project, options)`. - */ -export function init( - projectOrOptions: string | Readonly>, - optionalOptions?: Readonly>, ): InitializedExperiment { - const options = ((): Readonly> => { - if (typeof projectOrOptions === "string") { - return { ...optionalOptions, project: projectOrOptions }; - } else { - if (optionalOptions !== undefined) { - throw new Error( - "Cannot specify options struct as both parameters. Must call either init(project, options) or init(options).", - ); - } - return projectOrOptions; - } - })(); - const { project, experiment, @@ -4111,66 +4086,6 @@ export function init( return ret as InitializedExperiment; } -/** - * Alias for init(options). - */ -export function initExperiment( - options: Readonly>, -): InitializedExperiment; - -/** - * Alias for init(project, options). - */ -export function initExperiment( - project: string, - options?: Readonly>, -): InitializedExperiment; - -/** - * Combined overload implementation of `initExperiment`, which is an alias for - * `init`. Do not call this directly. Instead, call `initExperiment(options)` or - * `initExperiment(project, options)`. - */ -export function initExperiment( - projectOrOptions: string | Readonly>, - optionalOptions?: Readonly>, -): InitializedExperiment { - const options = ((): Readonly> => { - if (typeof projectOrOptions === "string") { - return { ...optionalOptions, project: projectOrOptions }; - } else { - if (optionalOptions !== undefined) { - throw new Error( - "Cannot specify options struct as both parameters. Must call either init(project, options) or init(options).", - ); - } - return projectOrOptions; - } - })(); - return init(options); -} - -/** - * @deprecated Use {@link init} instead. - */ -export function withExperiment( - project: string, - callback: (experiment: Experiment) => R, - options: Readonly & SetCurrentArg> = {}, -): R { - debugLogger - .forState(options.state) - .warn( - "withExperiment is deprecated and will be removed in a future version of braintrust. Simply create the experiment with `init`.", - ); - const experiment = init(project, options); - return callback(experiment); -} - -type UseOutputOption = { - useOutput?: IsLegacyDataset; -}; - declare global { // Set by the bt eval runner when CLI-controlled BTQL should be pushed down // into dataset-backed evals. @@ -4178,22 +4093,21 @@ declare global { var __bt_eval_internal_btql: Record | undefined; } -export type InitDatasetOptions = - FullLoginOptions & { - dataset?: string; - description?: string; - version?: string; - environment?: string; - snapshotName?: string; - projectId?: string; - metadata?: Record; - state?: BraintrustState; - _internal_btql?: Record; - } & UseOutputOption; +export type InitDatasetOptions = FullLoginOptions & { + dataset?: string; + description?: string; + version?: string; + environment?: string; + snapshotName?: string; + projectId?: string; + metadata?: Record; + state?: BraintrustState; + _internal_btql?: Record; +}; -export type FullInitDatasetOptions = { +export type FullInitDatasetOptions = { project?: string; -} & InitDatasetOptions; +} & InitDatasetOptions; async function getDatasetSnapshots( params: @@ -4416,52 +4330,11 @@ async function serializeDatasetForExperiment({ * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. * @param options.projectId The id of the project to create the dataset in. This takes precedence over `project` if specified. * @param options.metadata A dictionary with additional data about the dataset. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. - * @param options.useOutput (Deprecated) If true, records will be fetched from this dataset in the legacy format, with the "expected" field renamed to "output". This option will be removed in a future version of Braintrust. * @returns The newly created Dataset. */ -export function initDataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - options: Readonly>, -): Dataset; - -/** - * Legacy form of `initDataset` which accepts the project name as the first - * parameter, separately from the remaining options. - * - * See `initDataset(options)` for full details. - */ -export function initDataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - project: string, - options?: Readonly>, -): Dataset; - -/** - * Combined overload implementation of `initDataset`. Do not call this - * directly. Instead, call `initDataset(options)` or `initDataset(project, - * options)`. - */ -export function initDataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - projectOrOptions: string | Readonly>, - optionalOptions?: Readonly>, -): Dataset { - const options = ((): Readonly> => { - if (typeof projectOrOptions === "string") { - return { ...optionalOptions, project: projectOrOptions }; - } else { - if (optionalOptions !== undefined) { - throw new Error( - "Cannot specify options struct as both parameters. Must call either initDataset(project, options) or initDataset(options).", - ); - } - return projectOrOptions; - } - })(); - +export function initDataset( + options: Readonly, +): Dataset { const { project, dataset, @@ -4476,7 +4349,6 @@ export function initDataset< forceLogin, projectId, metadata, - useOutput: legacy, state: stateArg, _internal_btql, } = options; @@ -4563,7 +4435,6 @@ export function initDataset< stateArg ?? _globalState, lazyMetadata, typeof resolvedVersion === "string" ? resolvedVersion : undefined, - legacy, internalBtql, resolvedVersion instanceof LazyValue || normalizedEnvironment !== undefined || @@ -4591,26 +4462,6 @@ export function initDataset< return datasetObject; } -/** - * @deprecated Use {@link initDataset} instead. - */ -export function withDataset< - R, - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - project: string, - callback: (dataset: Dataset) => R, - options: Readonly> = {}, -): R { - debugLogger - .forState(options.state) - .warn( - "withDataset is deprecated and will be removed in a future version of braintrust. Simply create the dataset with `initDataset`.", - ); - const dataset = initDataset(project, options); - return callback(dataset); -} - // Note: the argument names *must* serialize the same way as the argument names // for the corresponding python function, because this function may be invoked // from arguments serialized elsewhere. @@ -6242,12 +6093,6 @@ export function wrapTraced any>( }, spanArgs)) as F; } -/** - * A synonym for `wrapTraced`. If you're porting from systems that use `traceable`, you can use this to - * make your codebase more consistent. - */ -export const traceable = wrapTraced; - /** * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", @@ -6259,19 +6104,25 @@ export function startSpan(args?: StartSpanArgs & OptionalStateArg): Span { return startSpanImpl(args); } +export function _internalStartSpan( + args?: InternalStartSpanArgs & OptionalStateArg, +): Span { + return startSpanImpl(args); +} + /** @internal Start a span whose initial row is merged with concurrent writes. */ export function _internalStartSpanWithInitialMerge( - args?: StartSpanArgs & OptionalStateArg, + args?: InternalStartSpanArgs & OptionalStateArg, ): Span { return startSpanImpl({ ...args, [INITIAL_SPAN_WRITE_AS_MERGE]: true, - } as StartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); + } as InternalStartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); } /** @internal Start a span with SDK-controlled context fields. */ export function _internalStartSpanWithContext( - args: StartSpanArgs & OptionalStateArg, + args: InternalStartSpanArgs & OptionalStateArg, context: Record, ): Span { return startSpanImpl({ @@ -6299,7 +6150,7 @@ export function setFetch(fetch: typeof globalThis.fetch): void { } function startSpanImpl( - args?: StartSpanArgs & OptionalStateArg & InternalSpanContextArg, + args?: InternalStartSpanArgs & OptionalStateArg & InternalSpanContextArg, ): Span { const state = args?.state ?? _globalState; @@ -6346,7 +6197,10 @@ function startSpanImpl( propagatedState, }); } else { - return parentObject.startSpan(args); + // The internal entrypoint still accepts exported Braintrust parent slugs. + // Span implementations support that runtime form even though it is no + // longer part of their public method signature. + return parentObject.startSpan(args as StartSpanArgs); } } @@ -6433,6 +6287,14 @@ async function* asyncGeneratorWithCurrent( } export function withParent( + parent: PropagationContext, + callback: () => R, + state: BraintrustState | undefined = undefined, +): R { + return (state ?? _globalState).currentParent.run(parent, () => callback()); +} + +export function _internalWithParent( parent: string | PropagationContext, callback: () => R, state: BraintrustState | undefined = undefined, @@ -6536,22 +6398,11 @@ function validateAndSanitizeExperimentLogPartialArgs( } } - if ("input" in event && event.input && "inputs" in event && event.inputs) { - throw new Error( - "Only one of input or inputs (deprecated) can be specified. Prefer input.", - ); - } - if ("tags" in event && event.tags) { validateTags(event.tags); } - if ("inputs" in event) { - const { inputs, ...rest } = event; - return { input: inputs, ...rest }; - } else { - return { ...event }; - } + return { ...event }; } /** @@ -6762,18 +6613,9 @@ async function resolveAttachmentsToBase64>( // handling special fields like 'id'). function validateAndSanitizeExperimentLogFullArgs( event: ExperimentLogFullArgs, - hasDataset: boolean, ): ExperimentLogFullArgs { - if ( - ("input" in event && - !isEmpty(event.input) && - "inputs" in event && - !isEmpty(event.inputs)) || - (!("input" in event) && !("inputs" in event)) - ) { - throw new Error( - "Exactly one of input or inputs (deprecated) must be specified. Prefer input.", - ); + if (!("input" in event)) { + throw new Error("input must be specified"); } if (isEmpty(event.output)) { @@ -6783,14 +6625,6 @@ function validateAndSanitizeExperimentLogFullArgs( throw new Error("scores must be specified"); } - if (hasDataset && event.datasetRecordId === undefined) { - throw new Error("datasetRecordId must be specified when using a dataset"); - } else if (!hasDataset && event.datasetRecordId !== undefined) { - throw new Error( - "datasetRecordId cannot be specified when not using a dataset", - ); - } - return event; } @@ -7122,7 +6956,7 @@ export class Experiment ); } - event = validateAndSanitizeExperimentLogFullArgs(event, !!this.dataset); + event = validateAndSanitizeExperimentLogFullArgs(event); const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); this.lastStartTime = span.end(); return span.id; @@ -7367,18 +7201,6 @@ export class Experiment async flush(): Promise { return await this.state.bgLogger().flush(); } - - /** - * @deprecated This function is deprecated. You can simply remove it from your code. - */ - public async close(): Promise { - debugLogger - .forState(this.state) - .warn( - "close is deprecated and will be removed in a future version of braintrust. It is now a no-op and can be removed", - ); - return this.id; - } } /** @@ -8002,10 +7824,6 @@ export class SpanImpl implements Span { return await this._state.bgLogger().flush(); } - public close(args?: EndSpanArgs): number { - return this.end(args); - } - public state(): BraintrustState { return this._state; } @@ -8086,9 +7904,7 @@ function splitLoggingData({ * * You should not create `Dataset` objects directly. Instead, use the `braintrust.initDataset()` method. */ -export class Dataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, -> extends ObjectFetcher> { +export class Dataset extends ObjectFetcher { private readonly lazyMetadata: LazyValue; private readonly __braintrust_dataset_marker = true; private newRecords = 0; @@ -8100,29 +7916,16 @@ export class Dataset< private state: BraintrustState, lazyMetadata: LazyValue, pinnedVersion?: string, - legacy?: IsLegacyDataset, _internal_btql?: Record, pinState?: DatasetPinState, ) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const isLegacyDataset = (legacy ?? - DEFAULT_IS_LEGACY_DATASET) as IsLegacyDataset; - if (isLegacyDataset) { - debugLogger - .forState(state) - .warn( - `Records will be fetched from this dataset in the legacy format, with the "expected" field renamed to "output". Please update your code to use "expected", and use \`braintrust.initDataset()\` with \`{ useOutput: false }\`, which will become the default in a future version of Braintrust.`, - ); - } super( "dataset", pinnedVersion, (r: AnyDatasetRecord) => - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ensureDatasetRecord( + ensureNewDatasetRecord( enrichAttachments(r, this.state), - isLegacyDataset, - ) as WithTransactionId>, + ) as WithTransactionId, _internal_btql, ); void this.__braintrust_dataset_marker; @@ -8213,13 +8016,9 @@ export class Dataset< private validateEvent({ metadata, - expected, - output, tags, }: { metadata?: Record; - expected?: unknown; - output?: unknown; tags?: string[]; }) { if (metadata !== undefined) { @@ -8230,12 +8029,6 @@ export class Dataset< } } - if (expected !== undefined && output !== undefined) { - throw new Error( - "Only one of expected or output (deprecated) can be specified. Prefer expected.", - ); - } - if (tags) { validateTags(tags); } @@ -8247,7 +8040,6 @@ export class Dataset< expected, metadata, tags, - output, origin, isMerge, }: { @@ -8256,18 +8048,15 @@ export class Dataset< expected?: unknown; metadata?: Record; tags?: string[]; - output?: unknown; origin?: ObjectReference; isMerge?: boolean; }): LazyValue { return new LazyValue(async () => { const dataset_id = await this.id; - const expectedValue = expected === undefined ? output : expected; - const args: BackgroundLogEvent = { id, input, - expected: expectedValue, + expected, tags, dataset_id, created: !isMerge ? new Date().toISOString() : undefined, //if we're merging/updating an event we will not add this ts @@ -8298,7 +8087,6 @@ export class Dataset< * JSON-serializable type, but its keys must be strings. * @param event.origin (Optional) a reference to the source object this dataset record was derived from. * @param event.id (Optional) a unique identifier for the event. If you don't provide one, Braintrust will generate one for you. - * @param event.output: (Deprecated) The output of your application. Use `expected` instead. * @returns The `id` of the logged record. */ public insert({ @@ -8307,7 +8095,6 @@ export class Dataset< metadata, tags, id, - output, origin, }: { readonly input?: unknown; @@ -8315,10 +8102,9 @@ export class Dataset< readonly tags?: string[]; readonly metadata?: Record; readonly id?: string; - readonly output?: unknown; readonly origin?: ObjectReference; }): string { - this.validateEvent({ metadata, expected, output, tags }); + this.validateEvent({ metadata, tags }); const rowId = id || uuidv4(); const args = this.createArgs( @@ -8328,7 +8114,6 @@ export class Dataset< expected, metadata, tags, - output, origin, isMerge: false, }), @@ -8365,7 +8150,7 @@ export class Dataset< readonly tags?: string[]; readonly metadata?: Record; }): string { - this.validateEvent({ metadata, expected, tags }); + this.validateEvent({ metadata, tags }); const args = this.createArgs( deepCopyEvent({ @@ -8561,18 +8346,6 @@ export class Dataset< return await this.state.bgLogger().flush(); } - /** - * @deprecated This function is deprecated. You can simply remove it from your code. - */ - public async close(): Promise { - debugLogger - .forState(this.state) - .warn( - "close is deprecated and will be removed in a future version of braintrust. It is now a no-op and can be removed", - ); - return this.id; - } - public static isDataset(data: unknown): data is Dataset { return ( typeof data === "object" && @@ -9337,7 +9110,7 @@ export class RemoteEvalParameters< } } -export type AnyDataset = Dataset; +export type AnyDataset = Dataset; /** * Summary of a score's performance. diff --git a/js/src/node/index.ts b/js/src/node/index.ts index 60e1efe09..e4a44adfe 100644 --- a/js/src/node/index.ts +++ b/js/src/node/index.ts @@ -56,4 +56,3 @@ import { configureNode } from "./config"; configureNode(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/openai-utils.ts b/js/src/openai-utils.ts index 5fa850915..9bc0cfa9c 100644 --- a/js/src/openai-utils.ts +++ b/js/src/openai-utils.ts @@ -5,8 +5,8 @@ import { isObject } from "../util/index"; // when OpenAI usage data does not include it. export const BRAINTRUST_CACHED_STREAM_METRIC = "__braintrust_cached_metric"; -export const LEGACY_CACHED_HEADER = "x-cached"; -export const X_CACHED_HEADER = "x-bt-cached"; +const LEGACY_CACHED_HEADER = "x-cached"; +const X_CACHED_HEADER = "x-bt-cached"; /** * Token name mappings for OpenAI metrics. @@ -63,7 +63,7 @@ export function parseMetricsFromUsage(usage: unknown): Record { return metrics; } -export function parseCachedHeader( +function parseCachedHeader( value: string | null | undefined, ): number | undefined { if (!value) { diff --git a/js/src/package-exports.test.ts b/js/src/package-exports.test.ts new file mode 100644 index 000000000..217d05ad7 --- /dev/null +++ b/js/src/package-exports.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, test } from "vitest"; + +const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +) as { exports: Record }; + +describe("package exports", () => { + test("publishes the curated entrypoints", () => { + expect(Object.keys(packageJson.exports).sort()).toEqual( + [ + ".", + "./apply-auto-instrumentation", + "./edge-light", + "./esbuild", + "./hook.mjs", + "./instrumentation", + "./next", + "./node", + "./package.json", + "./rollup", + "./vite", + "./vitest-evals-reporter", + "./webpack", + "./workerd", + ].sort(), + ); + }); + + test("does not publish legacy or implementation entrypoints", () => { + expect(packageJson.exports).not.toHaveProperty("./browser"); + expect(packageJson.exports).not.toHaveProperty("./util"); + expect(packageJson.exports).not.toHaveProperty("./webpack-loader"); + expect(packageJson.exports).not.toHaveProperty("./internal"); + expect(packageJson.exports).not.toHaveProperty("./internal/webpack-loader"); + }); +}); diff --git a/js/src/prompt-schemas.ts b/js/src/prompt-schemas.ts index 46ad5f307..c83c420f0 100644 --- a/js/src/prompt-schemas.ts +++ b/js/src/prompt-schemas.ts @@ -1,9 +1,3 @@ -import { z } from "zod/v3"; -import { - ToolFunctionDefinition as toolFunctionDefinitionSchema, - ChatCompletionMessageParam as chatCompletionMessageParamSchema, - ModelParams as modelParamsSchema, -} from "./generated_types"; import type { ToolFunctionDefinitionType as ToolFunctionDefinition, ChatCompletionMessageParamType, @@ -17,20 +11,6 @@ export type PromptContents = | { prompt: string } | { messages: ChatCompletionMessageParamType[] }; -const internalPromptContentsSchema = z.union([ - z.object({ - prompt: z.string(), - }), - z.object({ - messages: z.array(chatCompletionMessageParamSchema), - }), -]); -export const promptContentsSchema: z.ZodType< - PromptContents, - z.ZodTypeDef, - unknown -> = internalPromptContentsSchema; - export type PromptDefinition = PromptContents & { model: string; params?: ModelParamsType; @@ -38,36 +18,10 @@ export type PromptDefinition = PromptContents & { environments?: string[]; }; -const internalPromptDefinitionSchema = internalPromptContentsSchema.and( - z.object({ - model: z.string(), - params: modelParamsSchema.optional(), - templateFormat: z.enum(["mustache", "nunjucks", "none"]).optional(), - environments: z.array(z.string()).optional(), - }), -); -export const promptDefinitionSchema: z.ZodType< - PromptDefinition, - z.ZodTypeDef, - unknown -> = internalPromptDefinitionSchema; - export type PromptDefinitionWithTools = PromptDefinition & { tools?: ToolFunctionDefinition[]; }; -const internalPromptDefinitionWithToolsSchema = - internalPromptDefinitionSchema.and( - z.object({ - tools: z.array(toolFunctionDefinitionSchema).optional(), - }), - ); -export const promptDefinitionWithToolsSchema: z.ZodType< - PromptDefinitionWithTools, - z.ZodTypeDef, - unknown -> = internalPromptDefinitionWithToolsSchema; - export function promptDefinitionToPromptData( promptDefinition: PromptDefinition, rawTools?: ToolFunctionDefinition[], diff --git a/js/src/propagation.test.ts b/js/src/propagation.test.ts index 17b07dbac..936dfbd84 100644 --- a/js/src/propagation.test.ts +++ b/js/src/propagation.test.ts @@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { _exportsForTestingOnly, + _internalStartSpan, _injectIntoCarrier, extractTraceContextFromHeaders, initLogger, @@ -1026,14 +1027,14 @@ describe("inject / extract / round-trip", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: slug }); + const child = _internalStartSpan({ name: "child", parent: slug }); expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); child.end(); }); test("legacy parent slug (UUID) linked in hex mode", () => { - const logger = makeLogger(); + makeLogger(); const pSpan = uuidv4(); const pRoot = uuidv4(); const legacySlug = new SpanComponentsV3({ @@ -1044,7 +1045,7 @@ describe("inject / extract / round-trip", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: legacySlug }); + const child = _internalStartSpan({ name: "child", parent: legacySlug }); // Links to the slug's UUID ids; the child's own span id stays hex. expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); @@ -1064,7 +1065,7 @@ describe("inject / extract / round-trip", () => { root_span_id: pRoot, }).toStr(); - const child = startSpan({ name: "child", parent: legacySlug }); + const child = _internalStartSpan({ name: "child", parent: legacySlug }); expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); expect(child.spanId.length).toBe(16); @@ -1221,7 +1222,7 @@ describe("legacy UUID mode", () => { }); test("legacy parent slug (UUID) linked in legacy mode", () => { - const logger = initLogger({ projectName: "legacy-proj" }); + initLogger({ projectName: "legacy-proj" }); const pSpan = uuidv4(); const pRoot = uuidv4(); const legacySlug = new SpanComponentsV3({ @@ -1232,14 +1233,14 @@ describe("legacy UUID mode", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: legacySlug }); + const child = _internalStartSpan({ name: "child", parent: legacySlug }); expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); child.end(); }); test("hex parent slug linked in legacy mode", () => { - const logger = initLogger({ projectName: "legacy-proj" }); + initLogger({ projectName: "legacy-proj" }); const pSpan = "00f067aa0ba902b7"; // 8-byte hex const pRoot = "4bf92f3577b34da6a3ce929d0e0e4736"; // 16-byte hex const hexSlug = new SpanComponentsV4({ @@ -1250,7 +1251,7 @@ describe("legacy UUID mode", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: hexSlug }); + const child = _internalStartSpan({ name: "child", parent: hexSlug }); // Links to the slug's hex ids; the child's own span id stays UUID. expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); diff --git a/js/src/propagation.ts b/js/src/propagation.ts index a8246cce1..1733547aa 100644 --- a/js/src/propagation.ts +++ b/js/src/propagation.ts @@ -26,7 +26,7 @@ const DEFAULT_TRACE_FLAGS = "01"; * not sampled), kept raw so any future flag bits survive a parse -> format * round trip without per-bit handling. */ -export interface ParsedTraceparent { +interface ParsedTraceparent { traceId: string; spanId: string; traceFlags: string; diff --git a/js/src/public-types.test.ts b/js/src/public-types.test.ts index 0583994df..3a37a9cf7 100644 --- a/js/src/public-types.test.ts +++ b/js/src/public-types.test.ts @@ -1,69 +1,84 @@ -import { expectTypeOf, test } from "vitest"; -import { z } from "zod/v3"; +import { expect, expectTypeOf, test } from "vitest"; -import { - AttachmentReference, - braintrustStreamChunkSchema, - logs3OverflowUploadSchema, - promptContentsSchema, - promptDefinitionSchema, - promptDefinitionWithToolsSchema, - type BraintrustStreamChunk, - type EvalParameters, - type Logs3OverflowUpload, - type PromptContents, - type PromptDefinition, - type PromptDefinitionWithTools, +import * as publicExports from "./exports"; +import type { + ContextManager, + ContextParentSpanIds, + CurrentSpanStore, + Dataset, + DurableEvalStore, + EvalScorer, + EvalTask, + LangChainCallbackHandlerOptions, + PropagationContext, + Span, + StartSpanArgs, + TemplateRendererPlugin, + Trace, } from "./exports"; -import type { InferParameters } from "./eval-parameters"; -import type { AttachmentReferenceType } from "./generated_plain_types"; -import type { Prompt } from "./logger"; -import { - spanComponentsV3Schema, - type SpanComponentsV3Data, -} from "../util/span_identifier_v3"; import { - spanComponentsV4Schema, - type SpanComponentsV4Data, -} from "../util/span_identifier_v4"; + BraintrustLangChainCallbackHandler, + init, + initDataset, + withParent, +} from "./exports"; -test("exported validators preserve their public output types", () => { - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); +test("exports only intentional extension types", () => { + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf(publicExports.configureContextManager).toBeFunction(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf(BraintrustLangChainCallbackHandler).toBeConstructibleWith(); expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); + EvalTask + >().toBeFunction(); + expectTypeOf>().toBeFunction(); +}); + +test("does not expose runtime schemas or implementation helpers", () => { + for (const name of [ + "AttachmentReference", + "braintrustStreamChunkSchema", + "logs3OverflowUploadSchema", + "promptContentsSchema", + "promptDefinitionSchema", + "promptDefinitionWithToolsSchema", + "SpanImpl", + "IDGenerator", + "_exportsForTestingOnly", + "default", + ]) { + expect(publicExports).not.toHaveProperty(name); + } }); -test("evaluation parameters retain custom schema inference", () => { - const parameters = { - subject: z.string(), - model: { type: "model" as const }, - prompt: { type: "prompt" as const }, - } satisfies EvalParameters; +test("accepts only canonical v4 call shapes", () => { + if (false) { + init({ project: "project" }); + initDataset({ project: "project", dataset: "dataset" }); + + // @ts-expect-error The string-first overload was removed in v4. + init("project"); + // @ts-expect-error The string-first overload was removed in v4. + initDataset("project", { dataset: "dataset" }); + // @ts-expect-error Exported span slugs are internal integration plumbing. + withParent("exported-span", () => undefined); + + const dataset = null as unknown as Dataset; + // @ts-expect-error Dataset records use `expected`, not the legacy `output` alias. + dataset.insert({ input: "input", output: "expected" }); + + const span = null as unknown as Span; + // @ts-expect-error Spans end with `end()`; the `close()` alias was removed. + span.close(); + } - expectTypeOf>().toEqualTypeOf<{ - subject: string; - model: string; - prompt: Prompt; - }>(); + expect(true).toBe(true); }); diff --git a/js/src/sandbox.ts b/js/src/sandbox.ts index cfafa8c98..58c9f45be 100644 --- a/js/src/sandbox.ts +++ b/js/src/sandbox.ts @@ -7,7 +7,7 @@ import { type BraintrustState, _internalGetGlobalState } from "./logger"; * Configuration for a sandbox runtime. * @internal */ -export interface SandboxConfig { +interface SandboxConfig { /** The sandbox provider. Currently only "modal" is supported. */ provider: "modal"; /** Reference to the sandbox snapshot. */ @@ -18,7 +18,7 @@ export interface SandboxConfig { * Options for registering a sandbox function. * @internal */ -export interface RegisterSandboxOptions { +interface RegisterSandboxOptions { /** Group name for the sandbox functions. */ name: string; /** Name of the project to register the sandbox in. */ @@ -47,7 +47,7 @@ export interface RegisterSandboxOptions { * Result of registering a sandbox. * @internal */ -export interface RegisterSandboxResult { +interface RegisterSandboxResult { /** Project ID the sandbox is registered in. */ projectId: string; /** Registered eval functions discovered from this sandbox. */ diff --git a/js/src/template/registry.ts b/js/src/template/registry.ts index 6ebcace61..8c6faa704 100644 --- a/js/src/template/registry.ts +++ b/js/src/template/registry.ts @@ -97,7 +97,7 @@ class TemplatePluginRegistry { } } -export const templateRegistry = new TemplatePluginRegistry(); +const templateRegistry = new TemplatePluginRegistry(); /** * Register a template plugin and optionally activate it diff --git a/js/src/trace.ts b/js/src/trace.ts index d3e6b7610..fd40b6378 100644 --- a/js/src/trace.ts +++ b/js/src/trace.ts @@ -16,7 +16,7 @@ type SpanRecord = any; * Fetcher for spans by root_span_id, using the ObjectFetcher pattern. * Handles pagination automatically via cursor-based iteration. */ -export class SpanFetcher extends ObjectFetcher { +class SpanFetcher extends ObjectFetcher { constructor( objectType: "experiment" | "project_logs" | "playground_logs", private readonly _objectId: string, diff --git a/js/src/workerd/index.ts b/js/src/workerd/index.ts index 7f1f92300..3fa5cdb19 100644 --- a/js/src/workerd/index.ts +++ b/js/src/workerd/index.ts @@ -10,4 +10,3 @@ import { configureWorkerd } from "./config"; configureWorkerd(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts b/js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts deleted file mode 100644 index d483ed02b..000000000 --- a/js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts +++ /dev/null @@ -1,493 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { SpanTypeAttribute } from "../../../../util/index"; -import { startSpan, type CompiledPrompt } from "../../../logger"; -import { - INSTRUMENTATION_NAMES, - withSpanInstrumentationName, -} from "../../../span-origin"; -import { - extractAnthropicCacheTokens, - finalizeAnthropicTokens, - toNumericMetrics, -} from "../../anthropic-tokens-util"; -import { processInputAttachments } from "../../attachment-utils"; - -function detectProviderFromResult(result: { - providerMetadata?: Record; -}): string | undefined { - if (!result?.providerMetadata) { - return undefined; - } - - const keys = Object.keys(result.providerMetadata); - return keys?.at(0); -} - -function extractModelFromResult(result: { - response?: { modelId?: string }; - request?: { body?: { model?: string } }; -}): string | undefined { - if (result?.response?.modelId) { - return result.response.modelId; - } - - if (result?.request?.body?.model) { - return result.request.body.model; - } - - return undefined; -} - -function extractModelFromWrapGenerateCallback(model: { - modelId?: string; - config?: Record; - specificationVersion?: string; - provider?: string; - supportedUrls?: Record; -}): string | undefined { - return model?.modelId; -} - -function camelToSnake(str: string): string { - return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); -} - -function extractModelParameters( - params: Record, - excludeKeys: Set, -): Record { - const modelParams: Record = {}; - - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && !excludeKeys.has(key)) { - const snakeKey = camelToSnake(key); - modelParams[snakeKey] = value; - } - } - - return modelParams; -} - -function getNumberProperty(obj: unknown, key: string): number | undefined { - if (!obj || typeof obj !== "object" || !(key in obj)) { - return undefined; - } - const value = Reflect.get(obj, key); - return typeof value === "number" ? value : undefined; -} - -function normalizeUsageMetrics( - usage: unknown, - provider?: string, - providerMetadata?: Record, -): Record { - const metrics: Record = {}; - - // Standard AI SDK usage fields - const inputTokens = getNumberProperty(usage, "inputTokens"); - if (inputTokens !== undefined) { - metrics.prompt_tokens = inputTokens; - } - - const outputTokens = getNumberProperty(usage, "outputTokens"); - if (outputTokens !== undefined) { - metrics.completion_tokens = outputTokens; - } - - const totalTokens = getNumberProperty(usage, "totalTokens"); - if (totalTokens !== undefined) { - metrics.tokens = totalTokens; - } - - const reasoningTokens = getNumberProperty(usage, "reasoningTokens"); - if (reasoningTokens !== undefined) { - metrics.completion_reasoning_tokens = reasoningTokens; - } - - const cachedInputTokens = getNumberProperty(usage, "cachedInputTokens"); - if (cachedInputTokens !== undefined) { - metrics.prompt_cached_tokens = cachedInputTokens; - } - - // Anthropic-specific cache token handling - if (provider === "anthropic") { - const anthropicMetadata = providerMetadata?.anthropic as any; - - if (anthropicMetadata) { - const cacheReadTokens = - getNumberProperty(anthropicMetadata.usage, "cache_read_input_tokens") || - 0; - const cacheCreationTokens = - getNumberProperty( - anthropicMetadata.usage, - "cache_creation_input_tokens", - ) || 0; - - const cacheTokens = extractAnthropicCacheTokens( - cacheReadTokens, - cacheCreationTokens, - ); - Object.assign(metrics, cacheTokens); - - // Use the returned object: finalization can drop cache-creation metrics, - // and merging it back over `metrics` would keep them. - return toNumericMetrics(finalizeAnthropicTokens(metrics)); - } - } - - return metrics; -} - -function normalizeFinishReason(reason: any): string | undefined { - if (typeof reason !== "string") return undefined; - return reason.replace(/-/g, "_"); -} - -function buildAssistantOutputWithToolCalls(result: any, toolCalls: any[]) { - return [ - { - index: 0, - logprobs: null, - finish_reason: - normalizeFinishReason(result?.finishReason) ?? - (toolCalls.length ? "tool_calls" : undefined), - message: { - role: "assistant", - tool_calls: toolCalls.length > 0 ? toolCalls : undefined, - }, - }, - ]; -} - -function extractToolCallsFromSteps(steps: any[] | undefined) { - const toolCalls: any[] = []; - if (!Array.isArray(steps)) return toolCalls; - let idx = 0; - for (const step of steps) { - const blocks: any[] | undefined = (step as any)?.content; - if (!Array.isArray(blocks)) continue; - for (const block of blocks) { - if ( - block && - typeof block === "object" && - (block as any).type === "tool-call" - ) { - toolCalls.push({ - id: (block as any).toolCallId, - type: "function", - index: idx++, - function: { - name: (block as any).toolName, - arguments: - typeof (block as any).input === "string" - ? (block as any).input - : JSON.stringify((block as any).input ?? {}), - }, - }); - } - } - } - return toolCalls; -} - -function extractToolCallsFromBlocks(blocks: any[] | undefined) { - if (!Array.isArray(blocks)) return []; - return extractToolCallsFromSteps([{ content: blocks }] as any); -} - -function extractInput(params: any) { - return params?.prompt ?? params?.messages ?? params?.system; -} - -// Generic middleware interface that works with any AI SDK types -interface LanguageModelV2Middleware { - wrapGenerate?: (options: { - doGenerate: () => any; - doStream: () => any; - params: TCallOptions; - model: TModel; - }) => Promise; - wrapStream?: (options: { - doGenerate: () => any; - doStream: () => any; - params: TCallOptions; - model: TModel; - }) => Promise; -} - -/** - * Configuration options for the AI SDK middleware - */ -interface MiddlewareConfig { - /** Enable debug logging */ - debug?: boolean; - /** Name identifier for the middleware instance */ - name?: string; - /** Span info from loadPrompt for prompt version tracking */ - spanInfo?: CompiledPrompt<"chat">["span_info"]; -} - -// V2-specific exclude keys for extractModelParameters -const V2_EXCLUDE_KEYS = new Set([ - "prompt", // Already captured as input - "system", // Already captured as input - "messages", // Already captured as input - "model", // Already captured in metadata.model - "providerOptions", // Internal AI SDK configuration -]); - -/** - * Creates a Braintrust middleware for AI SDK v2 that automatically traces - * generateText and streamText calls with comprehensive metadata and metrics. - * - * @param config - Configuration options for the middleware - * @returns A middleware object compatible with AI SDK v2's wrapLanguageModel - * - * @deprecated The new `wrapAISDK` replaces this middleware. - * - * @example - * ```typescript - * import { wrapLanguageModel } from "ai"; - * import { openai } from "@ai-sdk/openai"; - * import { BraintrustMiddleware } from "braintrust"; - * - * const model = wrapLanguageModel({ - * model: openai("gpt-4"), - * middleware: BraintrustMiddleware({ debug: true, name: "MyMiddleware" }) - * }); - * ``` - * - */ -export function BraintrustMiddleware( - config: MiddlewareConfig = {}, -): LanguageModelV2Middleware { - return { - wrapGenerate: async ({ - doGenerate, - params, - model: modelFromWrapGenerate, - }) => { - // Extract and process input attachments - const rawInput = extractInput(params); - const processedInput = processInputAttachments(rawInput); - - const spanArgs = { - name: config.spanInfo?.name || "ai-sdk.doGenerate", - spanAttributes: { - type: SpanTypeAttribute.LLM, - ...(config.spanInfo?.spanAttributes || {}), - }, - event: { - input: processedInput, - metadata: { - ...extractModelParameters(params, V2_EXCLUDE_KEYS), - ...(config.spanInfo?.metadata || {}), - }, - }, - }; - - const span = startSpan( - withSpanInstrumentationName(spanArgs, INSTRUMENTATION_NAMES.AI_SDK), - ); - - try { - const result = await doGenerate(); - - const metadata: Record = {}; - - const provider = detectProviderFromResult(result); - if (provider !== undefined) { - metadata.provider = provider; - } - - if (result.finishReason !== undefined) { - metadata.finish_reason = result.finishReason; - } - - const model = extractModelFromResult(result); - if (model !== undefined) { - metadata.model = model; - } else if (modelFromWrapGenerate) { - // Use the model from the wrapGenerate call if it's not in the result - const modelId = extractModelFromWrapGenerateCallback( - modelFromWrapGenerate, - ); - if (modelId) { - metadata.model = modelId; - } - } - - let toolCalls = extractToolCallsFromSteps((result as any)?.steps); - if (!toolCalls || toolCalls.length === 0) { - toolCalls = extractToolCallsFromBlocks((result as any)?.content); - } - - span.log({ - output: - toolCalls.length > 0 - ? buildAssistantOutputWithToolCalls(result, toolCalls) - : (result as any)?.content, - metadata, - metrics: normalizeUsageMetrics( - result.usage, - provider, - result.providerMetadata, - ), - }); - - return result; - } catch (error) { - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - throw error; - } finally { - span.end(); - } - }, - wrapStream: async ({ doStream, params }) => { - // Extract and process input attachments - const rawInput = extractInput(params); - const processedInput = processInputAttachments(rawInput); - - const spanArgs = { - name: config.spanInfo?.name || "ai-sdk.doStream", - spanAttributes: { - type: SpanTypeAttribute.LLM, - ...(config.spanInfo?.spanAttributes || {}), - }, - event: { - input: processedInput, - metadata: { - ...extractModelParameters(params, V2_EXCLUDE_KEYS), - ...(config.spanInfo?.metadata || {}), - }, - }, - }; - - const span = startSpan( - withSpanInstrumentationName(spanArgs, INSTRUMENTATION_NAMES.AI_SDK), - ); - - try { - const { stream, ...rest } = await doStream(); - - const textChunks: string[] = []; - const toolBlocks: any[] = []; - let finalUsage: unknown = {}; - let finalFinishReason: unknown = undefined; - let providerMetadata: Record = {}; - - const transformStream = new TransformStream({ - transform(chunk: any, controller: any) { - try { - // Collect text deltas - if (chunk.type === "text-delta" && chunk.delta) { - textChunks.push(chunk.delta); - } - - // Collect tool call/result blocks for formatting later - if (chunk.type === "tool-call" || chunk.type === "tool-result") { - toolBlocks.push(chunk); - } - - // Capture final metadata - if (chunk.type === "finish") { - finalFinishReason = chunk.finishReason; - finalUsage = chunk.usage || {}; - providerMetadata = chunk.providerMetadata || {}; - } - - controller.enqueue(chunk); - } catch (error) { - // Log stream processing error - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - span.end(); - controller.error(error); - } - }, - - flush() { - try { - // Log the final aggregated result when stream completes - const generatedText = textChunks.join(""); - let output: unknown = generatedText - ? [{ type: "text", text: generatedText }] - : []; - - // Create a result object for provider detection - const resultForDetection = { - providerMetadata, - response: rest.response, - ...rest, - finishReason: finalFinishReason, - }; - - const metadata: Record = {}; - - const provider = detectProviderFromResult(resultForDetection); - if (provider !== undefined) { - metadata.provider = provider; - } - - if (finalFinishReason !== undefined) { - metadata.finish_reason = finalFinishReason; - } - - const model = extractModelFromResult(resultForDetection); - if (model !== undefined) { - metadata.model = model; - } - - // If tool calls streamed, prefer assistant tool_calls output - if (toolBlocks.length > 0) { - const toolCalls = extractToolCallsFromSteps([ - { content: toolBlocks }, - ] as any); - if (toolCalls.length > 0) { - output = buildAssistantOutputWithToolCalls( - resultForDetection, - toolCalls, - ); - } - } - - span.log({ - output, - metadata, - metrics: normalizeUsageMetrics( - finalUsage, - provider, - providerMetadata, - ), - }); - - span.end(); - } catch (error) { - // Log flush error - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - span.end(); - throw error; - } - }, - }); - - return { - stream: stream.pipeThrough(transformStream), - ...rest, - }; - } catch (error) { - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - span.end(); - throw error; - } - }, - }; -} diff --git a/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts b/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts deleted file mode 100644 index 705048346..000000000 --- a/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts +++ /dev/null @@ -1,362 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-assertions */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { startSpan } from "../../../logger"; -import { - INSTRUMENTATION_NAMES, - withSpanInstrumentationName, -} from "../../../span-origin"; -import { getCurrentUnixTimestamp, isEmpty } from "../../../util"; -import { - LEGACY_CACHED_HEADER, - parseCachedHeader, - X_CACHED_HEADER, -} from "../../oai"; - -/** - * Wrap an ai-sdk model (created with `.chat()`, `.completion()`, etc.) to add tracing. If Braintrust is - * not configured, this is a no-op - * - * @param model - * @deprecated You should use the new `wrapAISDK`. - * @returns The wrapped object. - */ -export function wrapAISDKModel(model: T): T { - const m = model as any; - if ( - m?.specificationVersion === "v1" && - typeof m?.provider === "string" && - typeof m?.modelId === "string" - ) { - return new BraintrustLanguageModelWrapper(m) as any as T; - } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported AI SDK model. Not wrapping."); - return model; - } -} - -class BraintrustLanguageModelWrapper { - supportsUrl?: (url: URL) => boolean; - - constructor(private model: any) { - // Only define supportsUrl if the model has this method - if (typeof this.model.supportsUrl === "function") { - this.supportsUrl = (url: URL) => this.model.supportsUrl!(url); - } - } - - get specificationVersion() { - return this.model.specificationVersion; - } - - get provider(): string { - return this.model.provider; - } - - get modelId(): string { - return this.model.modelId; - } - - get defaultObjectGenerationMode() { - return this.model.defaultObjectGenerationMode; - } - - get supportsImageUrls(): boolean | undefined { - return this.model.supportsImageUrls; - } - - get supportsStructuredOutputs(): boolean | undefined { - return this.model.supportsStructuredOutputs; - } - - // For the first cut, do not support custom span_info arguments. We can - // propagate those via async local storage - async doGenerate(options: any) { - const span = startSpan( - withSpanInstrumentationName( - { - name: "Chat Completion", - spanAttributes: { - type: "llm", - }, - }, - INSTRUMENTATION_NAMES.AI_SDK, - ), - ); - const { prompt, mode, ...rest } = options; - const startTime = getCurrentUnixTimestamp(); - - try { - const ret = await this.model.doGenerate(options); - span.log({ - input: postProcessPrompt(prompt), - metadata: { - model: this.modelId, - ...rest, - ...("tools" in mode && mode.tools - ? { tools: convertTools(mode.tools) } - : "tool" in mode && mode.tool - ? { tools: convertTools([mode.tool]) } - : {}), - }, - output: postProcessOutput(ret.text, ret.toolCalls, ret.finishReason), - metrics: { - time_to_first_token: getCurrentUnixTimestamp() - startTime, - tokens: !isEmpty(ret.usage) - ? ret.usage.promptTokens + ret.usage.completionTokens - : undefined, - prompt_tokens: ret.usage?.promptTokens, - completion_tokens: ret.usage?.completionTokens, - cached: parseCachedHeader( - ret.rawResponse?.headers?.[X_CACHED_HEADER] ?? - ret.rawResponse?.headers?.[LEGACY_CACHED_HEADER], - ), - }, - }); - return ret; - } finally { - span.end(); - } - } - - async doStream(options: any) { - const { prompt, mode, ...rest } = options; - const startTime = getCurrentUnixTimestamp(); - - const span = startSpan( - withSpanInstrumentationName( - { - name: "Chat Completion", - spanAttributes: { - type: "llm", - }, - }, - INSTRUMENTATION_NAMES.AI_SDK, - ), - ); - - span.log({ - input: postProcessPrompt(prompt), - metadata: { - model: this.modelId, - ...rest, - ...("tools" in mode && mode.tools - ? { tools: convertTools(mode.tools) } - : "tool" in mode && mode.tool - ? { tools: convertTools([mode.tool]) } - : {}), - }, - }); - - let ended = false; - const end = () => { - if (!ended) { - span.end(); - ended = true; - } - }; - - try { - const ret = await this.model.doStream(options); - - let time_to_first_token: number | undefined = undefined; - let usage: - | { - promptTokens: number; - completionTokens: number; - } - | undefined = undefined; - let fullText: string | undefined = undefined; - const toolCalls: Record = {}; - let finishReason: any | undefined = undefined; - return { - ...ret, - stream: ret.stream.pipeThrough( - new TransformStream({ - transform(chunk, controller) { - if (time_to_first_token === undefined) { - time_to_first_token = getCurrentUnixTimestamp() - startTime; - span.log({ metrics: { time_to_first_token } }); - } - switch (chunk.type) { - case "text-delta": - if (fullText === undefined) { - fullText = ""; - } - fullText += chunk.textDelta; - break; - case "tool-call": - toolCalls[chunk.toolCallId] = { - toolCallType: chunk.toolCallType, - toolCallId: chunk.toolCallId, - toolName: chunk.toolName, - args: chunk.args, - }; - break; - case "tool-call-delta": - if (toolCalls[chunk.toolCallId] === undefined) { - toolCalls[chunk.toolCallId] = { - toolCallType: chunk.toolCallType, - toolCallId: chunk.toolCallId, - toolName: chunk.toolName, - args: "", - }; - } - toolCalls[chunk.toolCallId].args += chunk.argsTextDelta; - break; - case "finish": - usage = chunk.usage; - finishReason = chunk.finishReason; - break; - } - - controller.enqueue(chunk); - }, - async flush(controller) { - span.log({ - output: postProcessOutput( - fullText, - Object.keys(toolCalls).length > 0 - ? Object.values(toolCalls) - : undefined, - finishReason!, - ), - metrics: { - time_to_first_token, - tokens: !isEmpty(usage) - ? usage.promptTokens + usage.completionTokens - : undefined, - prompt_tokens: usage?.promptTokens, - completion_tokens: usage?.completionTokens, - cached: parseCachedHeader( - ret.rawResponse?.headers?.[X_CACHED_HEADER] ?? - ret.rawResponse?.headers?.[LEGACY_CACHED_HEADER], - ), - }, - }); - end(); - controller.terminate(); - }, - }), - ), - }; - } finally { - end(); - } - } -} - -function convertTools(tools: Array): any[] { - return tools.map((tool) => { - const { type: _, ...rest } = tool; - return { - type: tool.type, - function: rest, - }; - }) as any[]; -} - -function postProcessPrompt(prompt: any): any[] { - return prompt.flatMap((message: any) => { - switch (message.role) { - case "system": - return [ - { - role: "system", - content: message.content, - }, - ]; - case "assistant": - const textPart = message.content.find( - (part: any) => part.type === "text", - ) as any | undefined; - const toolCallParts = message.content.filter( - (part: any) => part.type === "tool-call", - ) as any[]; - return [ - { - role: "assistant", - content: textPart?.text, - ...(toolCallParts.length > 0 - ? { - tool_calls: toolCallParts.map((part) => ({ - id: part.toolCallId, - function: { - name: part.toolName, - arguments: JSON.stringify(part.args), - }, - type: "function" as const, - })), - } - : {}), - }, - ]; - case "user": - return [ - { - role: "user", - content: message.content.map((part: any): any => { - switch (part.type) { - case "text": - return { - type: "text", - text: part.text, - ...(part.providerMetadata - ? { providerMetadata: part.providerMetadata } - : {}), - }; - case "image": - return { - type: "image_url", - image_url: { - url: part.image.toString(), - ...(part.providerMetadata - ? { providerMetadata: part.providerMetadata } - : {}), - }, - }; - default: - // We don't support files directly but also don't want to block them from being logged - return part as any; - } - }), - }, - ]; - case "tool": - return message.content.map((part: any) => ({ - role: "tool", - tool_call_id: part.toolCallId, - content: JSON.stringify(part.result), - })); - } - }); -} - -function postProcessOutput( - text: string | undefined, - toolCalls: any[] | undefined, - finishReason: any, -) { - return [ - { - index: 0, - message: { - role: "assistant", - content: text ?? "", - ...(toolCalls && toolCalls.length > 0 - ? { - tool_calls: toolCalls.map((toolCall) => ({ - id: toolCall.toolCallId, - function: { - name: toolCall.toolName, - arguments: toolCall.args, - }, - type: "function" as const, - })), - } - : {}), - }, - finish_reason: finishReason, - }, - ]; -} diff --git a/js/src/wrappers/ai-sdk/harness-agent-context.ts b/js/src/wrappers/ai-sdk/harness-agent-context.ts index f17f2a442..0aa4fbb5d 100644 --- a/js/src/wrappers/ai-sdk/harness-agent-context.ts +++ b/js/src/wrappers/ai-sdk/harness-agent-context.ts @@ -2,8 +2,8 @@ import iso from "../../isomorph"; import type { IsoAsyncLocalStorage, IsoTracingChannel } from "../../isomorph"; import { _internalGetGlobalState, + _internalStartSpan, currentSpan, - startSpan, updateSpan, type Span, type StartSpanArgs, @@ -359,9 +359,10 @@ export function startHarnessTurnChildSpan( args, INSTRUMENTATION_NAMES.AI_SDK, ); + const { parent: _ignoredParent, ...publicSpanArgs } = spanArgs; return typeof parent === "string" - ? startSpan({ ...spanArgs, parent }) - : parent.startSpan(spanArgs); + ? _internalStartSpan({ ...spanArgs, parent }) + : parent.startSpan(publicSpanArgs); } export function updateHarnessTurn( diff --git a/js/src/wrappers/ai-sdk/index.ts b/js/src/wrappers/ai-sdk/index.ts index b2058cd16..f9e1d86f3 100644 --- a/js/src/wrappers/ai-sdk/index.ts +++ b/js/src/wrappers/ai-sdk/index.ts @@ -1,6 +1,2 @@ export { wrapAISDK, wrapAgentClass } from "./ai-sdk"; export { braintrustAISDKTelemetry } from "./telemetry"; - -// TODO: remove in the next major release -export { wrapAISDKModel } from "./deprecated/wrapAISDKModel"; -export { BraintrustMiddleware } from "./deprecated/BraintrustMiddleware"; diff --git a/js/src/wrappers/exports.test.ts b/js/src/wrappers/exports.test.ts deleted file mode 100644 index 66850a3b3..000000000 --- a/js/src/wrappers/exports.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BraintrustMiddleware } from "./ai-sdk"; - -describe("ai-sdk exports", () => { - it("should always export BraintrustMiddleware as a function", () => { - expect(typeof BraintrustMiddleware).toBe("function"); - }); - - it("BraintrustMiddleware should return an object with wrapGenerate and wrapStream", () => { - const result = BraintrustMiddleware({}); - expect(result).toHaveProperty("wrapGenerate"); - expect(result).toHaveProperty("wrapStream"); - expect(typeof result.wrapGenerate).toBe("function"); - expect(typeof result.wrapStream).toBe("function"); - }); - - it("should handle conditional imports gracefully", () => { - // Test that imports don't throw errors regardless of AI SDK version - expect(() => { - const middleware = BraintrustMiddleware({ debug: true }); - - // Should be able to call the functions without errors - const { wrapGenerate, wrapStream } = middleware; - - expect(wrapGenerate).toBeDefined(); - expect(wrapStream).toBeDefined(); - }).not.toThrow(); - }); - - it("should export middleware functions that can be instantiated", () => { - const middleware = BraintrustMiddleware({}); - const { wrapGenerate, wrapStream } = middleware; - - // Should be functions that can be called (we don't test actual execution due to logger dependencies) - expect(typeof wrapGenerate).toBe("function"); - expect(typeof wrapStream).toBe("function"); - }); -}); diff --git a/js/src/wrappers/langchain/callback-handler.ts b/js/src/wrappers/langchain/callback-handler.ts index 51b073173..27bf90f28 100644 --- a/js/src/wrappers/langchain/callback-handler.ts +++ b/js/src/wrappers/langchain/callback-handler.ts @@ -89,13 +89,14 @@ export class BraintrustLangChainCallbackHandler { INSTRUMENTATION_NAMES.LANGCHAIN, ); - let span = parentSpan.startSpan(spanArgs); + const { parent: _ignoredParent, ...publicSpanArgs } = spanArgs; + let span = parentSpan.startSpan(publicSpanArgs); if ( !Object.is(this.options.logger, NOOP_SPAN) && Object.is(span, NOOP_SPAN) ) { - span = initLogger().startSpan(spanArgs); + span = initLogger().startSpan(publicSpanArgs); } this.spans.set(runId, span); diff --git a/js/src/wrappers/mastra.ts b/js/src/wrappers/mastra.ts index b3ff3b419..4ecaaacbe 100644 --- a/js/src/wrappers/mastra.ts +++ b/js/src/wrappers/mastra.ts @@ -75,7 +75,7 @@ interface MastraTracingEvent { } /** Subset of the `ObservabilityExporter` contract from `@mastra/core`. */ -export interface MastraObservabilityExporter { +interface MastraObservabilityExporter { name: string; init?(options: unknown): void; __setLogger?(logger: unknown): void; @@ -328,11 +328,12 @@ export class BraintrustObservabilityExporter implements MastraObservabilityExpor } } + const { parent: _ignoredParent, ...publicArgs } = args; const span = parentRecord - ? parentRecord.span.startSpan(args) + ? parentRecord.span.startSpan(publicArgs) : this.capturedParent - ? this.capturedParent.startSpan(args) - : startSpan(args); + ? this.capturedParent.startSpan(publicArgs) + : startSpan(publicArgs); const record: SpanRecord = { span, hasLoggedInput: false }; this.logPayload(record, exported); @@ -408,40 +409,3 @@ export class BraintrustObservabilityExporter implements MastraObservabilityExpor function logExporterError(err: unknown): void { debugLogger.warn("Mastra exporter failure:", err); } - -/** - * @deprecated Mastra is now instrumented through its own `ObservabilityExporter` - * contract instead of by wrapping the agent. This function does nothing and - * will be removed in the next major release. - * - * To capture Mastra spans in Braintrust, do one of: - * - * - **Auto-instrumentation**: run your app with - * `node --import braintrust/hook.mjs`. The loader installs - * `BraintrustObservabilityExporter` into every `new Mastra(...)` - * automatically. - * - **Manual wiring**: pass the exporter yourself: - * - * ```ts - * import { Mastra } from "@mastra/core"; - * import { Observability } from "@mastra/observability"; - * import { BraintrustObservabilityExporter } from "braintrust"; - * - * const mastra = new Mastra({ - * observability: new Observability({ - * configs: { - * default: { - * serviceName: "my-service", - * exporters: [new BraintrustObservabilityExporter()], - * }, - * }, - * }), - * }); - * ``` - */ -export function wrapMastraAgent( - agent: T, - _options?: { name?: string; span_name?: string }, -): T { - return agent; -} diff --git a/js/src/wrappers/node-test/suite.ts b/js/src/wrappers/node-test/suite.ts index c2ee794df..80cf58dae 100644 --- a/js/src/wrappers/node-test/suite.ts +++ b/js/src/wrappers/node-test/suite.ts @@ -1,4 +1,4 @@ -import { initExperiment, type Experiment } from "../../logger"; +import { init, type Experiment } from "../../logger"; import { runTracedEval } from "../shared/traced-eval"; import { summarizeAndFlush } from "../shared/flush"; import type { @@ -40,7 +40,8 @@ export function initNodeTestSuite(config: NodeTestSuiteConfig): NodeTestSuite { const experimentName = config.experimentName || `${config.projectName}-${new Date().toISOString()}`; - experiment = initExperiment(config.projectName, { + experiment = init({ + project: config.projectName, experiment: experimentName, }); return experiment; diff --git a/js/src/wrappers/node-test/test-helpers.ts b/js/src/wrappers/node-test/test-helpers.ts index 0047bf5ac..a455eae66 100644 --- a/js/src/wrappers/node-test/test-helpers.ts +++ b/js/src/wrappers/node-test/test-helpers.ts @@ -19,14 +19,12 @@ export async function setupNodeTestEnv(): Promise { await _exportsForTestingOnly.simulateLoginForTests(); const bgLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - vi.spyOn(logger, "initExperiment").mockImplementation( - (projectName: string, options?: any) => { - return _exportsForTestingOnly.initTestExperiment( - options?.experiment || "test-experiment", - projectName, - ); - }, - ); + vi.spyOn(logger, "init").mockImplementation((options) => { + return _exportsForTestingOnly.initTestExperiment( + options.experiment || "test-experiment", + options.project ?? options.projectId ?? "test-project", + ); + }); return bgLogger; } diff --git a/js/src/wrappers/oai.ts b/js/src/wrappers/oai.ts index 743460b7e..0599787e0 100644 --- a/js/src/wrappers/oai.ts +++ b/js/src/wrappers/oai.ts @@ -1,10 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { CompiledPrompt } from "../logger"; -import { - LEGACY_CACHED_HEADER, - parseCachedHeader, - X_CACHED_HEADER, -} from "../openai-utils"; import { responsesProxy } from "./oai_responses"; import type { ArgsOf, @@ -72,7 +67,7 @@ globalThis.__inherited_braintrust_wrap_openai = wrapOpenAI; type OpenAILike = OpenAIV4Client; -export function wrapOpenAIv4(openai: T): T { +function wrapOpenAIv4(openai: T): T { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const typedOpenai = openai as OpenAIV4Client; // Recover `this` for fallback methods so private fields and internal slots @@ -238,8 +233,6 @@ function wrapBetaChatCompletionStream

( }; } -export { LEGACY_CACHED_HEADER, parseCachedHeader, X_CACHED_HEADER }; - function wrapChatCompletion< P extends OpenAIChatCreateParams, C extends OpenAIChatCompletion | OpenAIChatStream, diff --git a/js/src/wrappers/vitest-evals/reporter.test.ts b/js/src/wrappers/vitest-evals/reporter.test.ts index 7b7385306..ccff70440 100644 --- a/js/src/wrappers/vitest-evals/reporter.test.ts +++ b/js/src/wrappers/vitest-evals/reporter.test.ts @@ -16,7 +16,7 @@ beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - vi.spyOn(logger, "initExperiment").mockImplementation( + vi.spyOn(logger, "init").mockImplementation( (projectOrOptions: string | any, options?: any) => { const experimentOptions = typeof projectOrOptions === "string" ? options : projectOrOptions; diff --git a/js/src/wrappers/vitest-evals/reporter.ts b/js/src/wrappers/vitest-evals/reporter.ts index 71ba251e3..ad3de38be 100644 --- a/js/src/wrappers/vitest-evals/reporter.ts +++ b/js/src/wrappers/vitest-evals/reporter.ts @@ -1,11 +1,6 @@ import type { Reporter, TestCase, TestModule, Vitest } from "vitest/node"; import { SpanTypeAttribute, isObject } from "../../../util"; -import { - initExperiment, - logError, - type Experiment, - type Span, -} from "../../logger"; +import { init, logError, type Experiment, type Span } from "../../logger"; import { configureNode } from "../../node/config"; import { summarizeAndFlush } from "../shared/flush"; @@ -202,7 +197,7 @@ export default class BraintrustVitestEvalsReporter implements Reporter { const experimentName = this.options.experimentName ?? `vitest-evals-${new Date().toISOString()}`; - this.experiment = initExperiment({ + this.experiment = init({ ...(projectId ? { projectId } : { project: projectName }), experiment: experimentName, metadata: this.options.metadata, diff --git a/js/src/wrappers/vitest/context-manager.ts b/js/src/wrappers/vitest/context-manager.ts index 03cb41799..88b6198b3 100644 --- a/js/src/wrappers/vitest/context-manager.ts +++ b/js/src/wrappers/vitest/context-manager.ts @@ -12,7 +12,7 @@ import type { Dataset, Experiment } from "../../logger"; * - flushPromise/flushResolved: Coordination for experiment flushing */ export interface VitestExperimentContext { - dataset: Dataset | undefined; + dataset: Dataset | undefined; experiment: Experiment; datasetExamples: Map; parent?: VitestExperimentContext; // Link to parent describe @@ -65,7 +65,7 @@ class VitestContextManager { } createChildContext( - dataset: Dataset | undefined, + dataset: Dataset | undefined, experiment: Experiment, ): VitestExperimentContext { const parent = this.getCurrentContext(); diff --git a/js/src/wrappers/vitest/vitest-wrapper-example.test.ts b/js/src/wrappers/vitest/vitest-wrapper-example.test.ts index c646c0fdf..452e6e8ca 100644 --- a/js/src/wrappers/vitest/vitest-wrapper-example.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper-example.test.ts @@ -11,7 +11,7 @@ await _exportsForTestingOnly.simulateLoginForTests(); const moduleBackgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); vitest.vi - .spyOn(logger, "initExperiment") + .spyOn(logger, "init") .mockImplementation((projectName: string, options?: any) => { return _exportsForTestingOnly.initTestExperiment( options?.experiment || "test-experiment", diff --git a/js/src/wrappers/vitest/vitest-wrapper-span.test.ts b/js/src/wrappers/vitest/vitest-wrapper-span.test.ts index 47cbcbdbe..a7995b20a 100644 --- a/js/src/wrappers/vitest/vitest-wrapper-span.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper-span.test.ts @@ -11,7 +11,7 @@ await _exportsForTestingOnly.simulateLoginForTests(); const moduleBackgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); vitest.vi - .spyOn(logger, "initExperiment") + .spyOn(logger, "init") .mockImplementation((projectName: string, options?: any) => { return _exportsForTestingOnly.initTestExperiment( options?.experiment || "test-experiment", diff --git a/js/src/wrappers/vitest/vitest-wrapper.test.ts b/js/src/wrappers/vitest/vitest-wrapper.test.ts index 6154b21b0..d3d7bed6f 100644 --- a/js/src/wrappers/vitest/vitest-wrapper.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper.test.ts @@ -32,13 +32,13 @@ _exportsForTestingOnly.setInitialTestState(); await _exportsForTestingOnly.simulateLoginForTests(); _exportsForTestingOnly.useTestBackgroundLogger(); -// ✅ STEP 2: Mock initDataset and initExperiment to avoid network calls +// ✅ STEP 2: Mock initDataset and init to avoid network calls vi.spyOn(logger, "initDataset").mockReturnValue({ insert: vi.fn(() => "test-example-id"), } as any); -const initExperimentSpy = vi - .spyOn(logger, "initExperiment") +const initSpy = vi + .spyOn(logger, "init") .mockImplementation((projectOrOptions: string | any, options?: any) => { const experimentOptions = typeof projectOrOptions === "string" ? options : projectOrOptions; @@ -451,11 +451,11 @@ describe("Project selection", () => { } beforeEach(() => { - initExperimentSpy.mockClear(); + initSpy.mockClear(); _resetContextManager(); }); - test("wrapVitest passes projectId to initExperiment", async () => { + test("wrapVitest passes projectId to init", async () => { const bt = wrapVitest(makeFakeVitestMethods(), { projectId: "project-id-123", displaySummary: false, @@ -465,7 +465,7 @@ describe("Project selection", () => { bt.test("uses project id", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ projectId: "project-id-123", experiment: expect.stringMatching(/^project id suite-/), @@ -484,15 +484,12 @@ describe("Project selection", () => { bt.test("uses project id", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ projectId: "project-id-123", }), ); - expect(initExperimentSpy).not.toHaveBeenCalledWith( - "project-name", - expect.anything(), - ); + expect(initSpy).not.toHaveBeenCalledWith("project-name", expect.anything()); }); test("wrapVitest preserves projectName behavior when projectId is absent", async () => { @@ -505,9 +502,9 @@ describe("Project selection", () => { bt.test("uses project name", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( - "project-name", + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ + project: "project-name", experiment: expect.stringMatching(/^project name suite-/), }), ); @@ -522,9 +519,9 @@ describe("Project selection", () => { bt.test("uses suite name", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( - "fallback suite", + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ + project: "fallback suite", experiment: expect.stringMatching(/^fallback suite-/), }), ); diff --git a/js/src/wrappers/vitest/wrapper.ts b/js/src/wrappers/vitest/wrapper.ts index 4be2adf5b..c0f0ee381 100644 --- a/js/src/wrappers/vitest/wrapper.ts +++ b/js/src/wrappers/vitest/wrapper.ts @@ -1,4 +1,4 @@ -import { initExperiment } from "../../logger"; +import { init } from "../../logger"; import type { TestConfig, TestContext, @@ -215,11 +215,12 @@ export function wrapDescribe( const experimentName = `${suiteName}-${new Date().toISOString()}`; const experiment = config.projectId - ? initExperiment({ + ? init({ projectId: config.projectId, experiment: experimentName, }) - : initExperiment(config.projectName || suiteName, { + : init({ + project: config.projectName || suiteName, experiment: experimentName, }); diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index 3941302be..6241d23d6 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -20,7 +20,7 @@ import type { Options } from "tsup"; * ## How It Works * * 1. Downloads the latest published version from npm - * 2. Extracts the .d.ts files for each entrypoint (main, browser, util) + * 2. Extracts the .d.ts files for each built entrypoint * 3. Parses both published and current .d.ts files using TypeScript Compiler API * 4. Compares exported symbols (functions, classes, interfaces, types, etc.) * 5. Fails if breaking changes are detected in non-major version bumps @@ -77,10 +77,10 @@ async function getEntrypointsFromTsupConfig(): Promise< entrypoints.push({ name, typesPath }); } } else if (typeof entry === "object") { - // entry is a record like { main: 'src/index.ts' } - for (const [key, entryFile] of Object.entries(entry)) { + // Object entry keys determine the emitted path, including nested paths. + for (const key of Object.keys(entry)) { const name = key; - const typesPath = getTypesPath(String(entryFile), outDir); + const typesPath = path.join(outDir, `${key}.d.ts`); entrypoints.push({ name, typesPath }); } } @@ -97,10 +97,8 @@ function getEntrypointName(entryFile: string, outDir: string): string { // Map common patterns to friendly names if (entryFile.includes("src/node/index.ts")) return "main"; - if (entryFile.includes("src/browser/index.ts")) return "browser"; if (entryFile.includes("src/edge-light/index.ts")) return "edge-light"; if (entryFile.includes("src/workerd/index.ts")) return "workerd"; - if (entryFile.includes("util/index.ts")) return "util"; // Default to basename return basename; @@ -2951,13 +2949,14 @@ describe("API Compatibility", () => { test("keeps public declarations free of expanded Zod schema graphs", () => { const declarationRoot = path.join(__dirname, "..", ".."); const publicDeclarationPaths = [ - "dist/index.d.ts", - "dist/browser.d.ts", - "util/dist/index.d.ts", + ...new Set(ENTRYPOINTS.map(({ typesPath }) => typesPath)), ]; for (const declarationTypesPath of publicDeclarationPaths) { const declarationPath = path.join(declarationRoot, declarationTypesPath); + if (!fs.existsSync(declarationPath)) { + continue; + } const declaration = fs.readFileSync(declarationPath, "utf8"); expect(declaration).not.toMatch(/z\.infer { "promptDefinitionSchema", "promptDefinitionWithToolsSchema", ]) { - expect(mainDeclaration).toMatch( - new RegExp(`declare const ${schemaName}: z\\.ZodType<`), - ); - } - - const utilDeclaration = fs.readFileSync( - path.join(declarationRoot, "util/dist/index.d.ts"), - "utf8", - ); - for (const schemaName of [ - "spanComponentsV3Schema", - "spanComponentsV4Schema", - "spanObjectTypeV3EnumSchema", - ]) { - expect(utilDeclaration).toMatch( - new RegExp(`declare const ${schemaName}: z\\.ZodType<`), + expect(mainDeclaration).not.toMatch( + new RegExp(`(?:declare|export) const ${schemaName}(?::|\\s*=)`), ); } }); diff --git a/js/tests/auto-instrumentations/error-handling.test.ts b/js/tests/auto-instrumentations/error-handling.test.ts index 2c31588a4..7bbccfe75 100644 --- a/js/tests/auto-instrumentations/error-handling.test.ts +++ b/js/tests/auto-instrumentations/error-handling.test.ts @@ -73,7 +73,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -82,7 +82,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -133,7 +133,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -142,7 +142,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -184,7 +184,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -193,7 +193,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -228,7 +228,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -237,7 +237,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -281,7 +281,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -290,7 +290,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -336,7 +336,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -345,7 +345,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -400,7 +400,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -409,7 +409,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/event-content.test.ts b/js/tests/auto-instrumentations/event-content.test.ts index 5a2fdd100..81acdee76 100644 --- a/js/tests/auto-instrumentations/event-content.test.ts +++ b/js/tests/auto-instrumentations/event-content.test.ts @@ -75,7 +75,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -84,7 +84,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -149,7 +149,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -158,7 +158,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -216,7 +216,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -225,7 +225,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -296,7 +296,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -305,7 +305,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -357,7 +357,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -366,7 +366,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -411,7 +411,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -420,7 +420,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -467,7 +467,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -476,7 +476,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -525,7 +525,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -534,7 +534,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -652,7 +652,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -661,7 +661,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -727,7 +727,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -736,7 +736,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -831,7 +831,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -840,7 +840,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -938,7 +938,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -947,7 +947,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1045,7 +1045,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1054,7 +1054,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1132,7 +1132,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1141,7 +1141,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1233,7 +1233,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1242,7 +1242,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1340,7 +1340,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1349,7 +1349,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1416,7 +1416,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1425,7 +1425,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1514,7 +1514,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1523,7 +1523,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1603,7 +1603,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1612,7 +1612,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1699,7 +1699,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1708,7 +1708,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/function-behavior.test.ts b/js/tests/auto-instrumentations/function-behavior.test.ts index 1797252b5..0881a6d53 100644 --- a/js/tests/auto-instrumentations/function-behavior.test.ts +++ b/js/tests/auto-instrumentations/function-behavior.test.ts @@ -63,7 +63,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -72,7 +72,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -131,7 +131,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -140,7 +140,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -178,7 +178,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -187,7 +187,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -230,7 +230,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -239,7 +239,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -284,7 +284,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -293,7 +293,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -338,7 +338,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -347,7 +347,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -392,7 +392,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -401,7 +401,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -438,7 +438,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -447,7 +447,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -489,7 +489,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -498,7 +498,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/multiple-instrumentations.test.ts b/js/tests/auto-instrumentations/multiple-instrumentations.test.ts index 2b48ff0a6..bb53cbe60 100644 --- a/js/tests/auto-instrumentations/multiple-instrumentations.test.ts +++ b/js/tests/auto-instrumentations/multiple-instrumentations.test.ts @@ -101,7 +101,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -110,7 +110,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -169,7 +169,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -178,7 +178,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -234,7 +234,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -243,7 +243,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -314,7 +314,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -323,7 +323,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -402,7 +402,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); // Add custom instrumentation config @@ -427,7 +427,7 @@ describe("Multiple Instrumentations", () => { outfile, format: "esm", plugins: [ - esbuildPlugin({ + braintrustEsbuildPlugin({ browser: false, instrumentations: [customConfig], }), @@ -488,7 +488,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -497,7 +497,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/next-config.test.ts b/js/tests/auto-instrumentations/next-config.test.ts index 91d96d401..7ebefe3ce 100644 --- a/js/tests/auto-instrumentations/next-config.test.ts +++ b/js/tests/auto-instrumentations/next-config.test.ts @@ -1,14 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../../src/auto-instrumentations/bundler/webpack.js", () => ({ - webpackPlugin: vi.fn((options: unknown) => ({ + braintrustWebpackPlugin: vi.fn((options: unknown) => ({ apply: () => {}, name: "braintrust-test-webpack-plugin", options, })), })); -import { webpackPlugin } from "../../src/auto-instrumentations/bundler/webpack.js"; +import { braintrustWebpackPlugin } from "../../src/auto-instrumentations/bundler/webpack.js"; import { wrapNextjsConfigWithBraintrust } from "../../src/auto-instrumentations/bundler/next.js"; const originalArgv = [...process.argv]; @@ -45,7 +45,7 @@ describe("wrapNextjsConfigWithBraintrust", () => { const result = config.webpack({ plugins: [] }, { isServer: false }); expect(userWebpack).toHaveBeenCalledOnce(); - expect(webpackPlugin).toHaveBeenCalledWith({ browser: true }); + expect(braintrustWebpackPlugin).toHaveBeenCalledWith({ browser: true }); expect(result.plugins).toHaveLength(2); expect(result.plugins[0]).toEqual({ name: "user-plugin" }); expect(result.plugins[1].options).toEqual({ browser: true }); @@ -183,8 +183,8 @@ describe("wrapNextjsConfigWithBraintrust", () => { }, { resolve: (specifier: string) => { - if (specifier === "braintrust/webpack-loader") { - return "/braintrust/webpack-loader.cjs"; + if (specifier === "braintrust/package.json") { + return "/braintrust/package.json"; } throw new Error(`Cannot resolve module ${specifier}`); diff --git a/js/tests/auto-instrumentations/runtime-execution.test.ts b/js/tests/auto-instrumentations/runtime-execution.test.ts index eb882dd3f..991826798 100644 --- a/js/tests/auto-instrumentations/runtime-execution.test.ts +++ b/js/tests/auto-instrumentations/runtime-execution.test.ts @@ -63,7 +63,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -72,7 +72,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -122,7 +122,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -131,7 +131,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -188,7 +188,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -197,7 +197,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -233,7 +233,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { vitePlugin } = + const { braintrustVitePlugin } = await import("../../src/auto-instrumentations/bundler/vite.js"); await viteBuild({ @@ -248,7 +248,7 @@ describe("Runtime Execution of Bundled Code", () => { emptyOutDir: true, minify: false, }, - plugins: [vitePlugin({ browser: false })], + plugins: [braintrustVitePlugin({ browser: false })], logLevel: "error", resolve: { preserveSymlinks: true, @@ -292,7 +292,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { vitePlugin } = + const { braintrustVitePlugin } = await import("../../src/auto-instrumentations/bundler/vite.js"); await viteBuild({ @@ -307,7 +307,7 @@ describe("Runtime Execution of Bundled Code", () => { emptyOutDir: true, minify: false, }, - plugins: [vitePlugin({ browser: false })], + plugins: [braintrustVitePlugin({ browser: false })], logLevel: "error", resolve: { preserveSymlinks: true, @@ -344,7 +344,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { rollupPlugin } = + const { braintrustRollupPlugin } = await import("../../src/auto-instrumentations/bundler/rollup.js"); // Simple resolver plugin @@ -363,7 +363,7 @@ describe("Runtime Execution of Bundled Code", () => { const bundle = await rollup({ input: entryPoint, - plugins: [resolverPlugin, rollupPlugin({ browser: false })], + plugins: [resolverPlugin, braintrustRollupPlugin({ browser: false })], external: [], preserveSymlinks: true, }); @@ -413,7 +413,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { rollupPlugin } = + const { braintrustRollupPlugin } = await import("../../src/auto-instrumentations/bundler/rollup.js"); const resolverPlugin = { @@ -431,7 +431,7 @@ describe("Runtime Execution of Bundled Code", () => { const bundle = await rollup({ input: entryPoint, - plugins: [resolverPlugin, rollupPlugin({ browser: false })], + plugins: [resolverPlugin, braintrustRollupPlugin({ browser: false })], external: [], preserveSymlinks: true, }); @@ -476,7 +476,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -485,7 +485,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -530,7 +530,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -539,7 +539,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/streaming-and-responses.test.ts b/js/tests/auto-instrumentations/streaming-and-responses.test.ts index e994de00e..aafd66bf5 100644 --- a/js/tests/auto-instrumentations/streaming-and-responses.test.ts +++ b/js/tests/auto-instrumentations/streaming-and-responses.test.ts @@ -155,7 +155,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -164,7 +164,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -216,7 +216,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -225,7 +225,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -310,7 +310,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -319,7 +319,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -400,7 +400,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -409,7 +409,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -461,7 +461,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -470,7 +470,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -554,7 +554,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -563,7 +563,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/transformation.test.ts b/js/tests/auto-instrumentations/transformation.test.ts index 0a21456cd..18800bdfd 100644 --- a/js/tests/auto-instrumentations/transformation.test.ts +++ b/js/tests/auto-instrumentations/transformation.test.ts @@ -630,7 +630,7 @@ describe("Orchestrion Transformation Tests", () => { it.each([ ["browser", "browser", { browser: true }], - ["legacy-browser", "browser", { useDiagnosticChannelCompatShim: true }], + ["legacy-browser", "browser", { browser: true }], ["edge", "neutral", { browser: true }], ] as const)( "should keep Mastra %s bundles free of Node-only patches", @@ -933,13 +933,7 @@ describe("Orchestrion Transformation Tests", () => { }); it.each([ - [ - "skip", - "legacy browser", - "web", - { useDiagnosticChannelCompatShim: true }, - false, - ], + ["skip", "legacy browser", "web", { browser: true }, false], ["skip", "browser", "web", { browser: true }, false], ["apply", "node", "node", { browser: false }, true], ] as const)( diff --git a/js/tsup.config.ts b/js/tsup.config.ts index 416ef9679..760f06fbf 100644 --- a/js/tsup.config.ts +++ b/js/tsup.config.ts @@ -14,6 +14,7 @@ export default defineConfig([ { entry: { index: "src/node/index.ts", + "instrumentation/index": "src/instrumentation/index.ts", "apply-auto-instrumentation": "src/node/apply-auto-instrumentation-entry.ts", "vitest-evals-reporter": "src/wrappers/vitest-evals/reporter.ts", @@ -32,22 +33,6 @@ export default defineConfig([ define, clean: true, }, - { - entry: ["util/index.ts"], - format: ["cjs", "esm"], - outDir: "util/dist", - external: ["esbuild", "prettier", "typescript", "zod"], - removeNodeProtocol: false, - dts: { - // Split DTS generation to reduce memory usage - compilerOptions: { - skipLibCheck: true, - }, - }, - splitting: true, - define, - clean: true, - }, // Browser/edge entrypoints { entry: { @@ -67,20 +52,6 @@ export default defineConfig([ define, clean: false, }, - { - entry: ["src/instrumentation/index.ts"], - format: ["cjs", "esm"], - outDir: "dist/instrumentation", - external: ["@braintrust/instrumentation-core", "zod"], - dts: { - compilerOptions: { - skipLibCheck: true, - }, - }, - splitting: false, - define, - clean: true, - }, { entry: [ "src/auto-instrumentations/index.ts", diff --git a/js/util/object.ts b/js/util/object.ts index 941c6264a..074aa6229 100644 --- a/js/util/object.ts +++ b/js/util/object.ts @@ -23,7 +23,6 @@ export type OtherExperimentLogFields = { classifications?: Record; metadata: Record; metrics: Record; - datasetRecordId: string; origin: ObjectReferenceType; span_attributes: Record; [ASYNC_SCORING_CONTROL_FIELD]: AsyncScoringControl; @@ -43,7 +42,7 @@ export type ExperimentLogFullArgs = Partial< export type LogFeedbackFullArgs = IdField & Partial< - Omit & { + Omit & { comment: string; source: Source; } diff --git a/js/util/score.ts b/js/util/score.ts index 1c8a4754e..53da7063a 100644 --- a/js/util/score.ts +++ b/js/util/score.ts @@ -40,11 +40,6 @@ export interface Score { name: string; score: number | null; metadata?: Record; - // DEPRECATION_NOTICE: this field is deprecated, as errors are propagated up to the caller. - /** - * @deprecated - */ - error?: unknown; } export type ScorerArgs = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6deed80a..2e0e646d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: e2e: devDependencies: + '@braintrust/browser': + specifier: workspace:^ + version: link:../integrations/browser-js '@braintrust/langchain-js': specifier: workspace:^ version: link:../integrations/langchain-js