From 91bb347d7f309b39be49adc405138df0800b0ecd Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 10:31:47 +0200 Subject: [PATCH 1/8] feat(bundler-plugins): Add `braintrust{Bundler}Plugin` aliases for bundler plugins and deprecate rest --- js/README.md | 16 ++++++++-------- js/src/auto-instrumentations/README.md | 16 ++++++---------- .../auto-instrumentations/bundler/esbuild.ts | 11 ++++++++--- .../auto-instrumentations/bundler/rollup.ts | 11 ++++++++--- js/src/auto-instrumentations/bundler/vite.ts | 11 ++++++++--- .../auto-instrumentations/bundler/webpack.ts | 11 ++++++++--- js/src/auto-instrumentations/index.ts | 4 ++-- .../bundler-exports.test.ts | 19 +++++++++++++++++++ 8 files changed, 67 insertions(+), 32 deletions(-) create mode 100644 js/tests/auto-instrumentations/bundler-exports.test.ts diff --git a/js/README.md b/js/README.md index 7a1b21afc..6cd1b0a45 100644 --- a/js/README.md +++ b/js/README.md @@ -61,40 +61,40 @@ Use a bundler plugin: Vite: ```ts -import { vitePlugin } from "braintrust/vite"; +import { braintrustVitePlugin } from "braintrust/vite"; export default { - plugins: [vitePlugin()], + plugins: [braintrustVitePlugin()], }; ``` Webpack: ```js -const { webpackPlugin } = require("braintrust/webpack"); +const { braintrustWebpackPlugin } = require("braintrust/webpack"); module.exports = { - plugins: [webpackPlugin()], + plugins: [braintrustWebpackPlugin()], }; ``` esbuild: ```ts -import { esbuildPlugin } from "braintrust/esbuild"; +import { braintrustEsbuildPlugin } from "braintrust/esbuild"; await esbuild.build({ - plugins: [esbuildPlugin()], + plugins: [braintrustEsbuildPlugin()], }); ``` Rollup: ```ts -import { rollupPlugin } from "braintrust/rollup"; +import { braintrustRollupPlugin } from "braintrust/rollup"; export default { - plugins: [rollupPlugin()], + plugins: [braintrustRollupPlugin()], }; ``` diff --git a/js/src/auto-instrumentations/README.md b/js/src/auto-instrumentations/README.md index 4fbc04f2a..83062bea1 100644 --- a/js/src/auto-instrumentations/README.md +++ b/js/src/auto-instrumentations/README.md @@ -245,11 +245,11 @@ When using bundler plugins (Vite, Webpack, etc.) in Node.js: ```javascript // vite.config.js -import { vitePlugin } from "@braintrust/auto-instrumentations/bundler/vite"; +import { braintrustVitePlugin } from "braintrust/vite"; export default { plugins: [ - vitePlugin({ browser: false }), // IMPORTANT: Set browser: false for Node.js + braintrustVitePlugin({ browser: false }), // IMPORTANT: Set browser: false for Node.js ], }; ``` @@ -262,11 +262,11 @@ Setting `browser: false` ensures the code-transformer injects `node:diagnostics_ ```javascript // vite.config.js -import { vitePlugin } from "@braintrust/auto-instrumentations/bundler/vite"; +import { braintrustVitePlugin } from "braintrust/vite"; export default { plugins: [ - vitePlugin({ browser: true }), // Use browser: true for browser builds + braintrustVitePlugin({ browser: true }), // Use browser: true for browser builds ], }; ``` @@ -440,14 +440,10 @@ channel.subscribe({ #### TypeScript Errors with Bundler Plugins -Some bundlers may have TypeScript resolution issues with the plugin imports. Use `.js` extension in imports: +If TypeScript has resolution issues with direct internal imports, use the public Braintrust bundler entrypoints: ```javascript -// Instead of: -import { vitePlugin } from "@braintrust/auto-instrumentations/bundler/vite"; - -// Use: -import { vitePlugin } from "@braintrust/auto-instrumentations/bundler/vite.js"; +import { braintrustVitePlugin } from "braintrust/vite"; ``` #### ESM vs CJS Mixing diff --git a/js/src/auto-instrumentations/bundler/esbuild.ts b/js/src/auto-instrumentations/bundler/esbuild.ts index 83278dbac..c63c1d631 100644 --- a/js/src/auto-instrumentations/bundler/esbuild.ts +++ b/js/src/auto-instrumentations/bundler/esbuild.ts @@ -3,10 +3,10 @@ * * Usage: * ```typescript - * import { esbuildPlugin } from '@braintrust/auto-instrumentations/bundler/esbuild'; + * import { braintrustEsbuildPlugin } from 'braintrust/esbuild'; * * await esbuild.build({ - * plugins: [esbuildPlugin()], + * plugins: [braintrustEsbuildPlugin()], * }); * ``` * @@ -21,4 +21,9 @@ import { unplugin, type BundlerPluginOptions } from "./plugin"; export type EsbuildPluginOptions = BundlerPluginOptions; -export const esbuildPlugin = unplugin.esbuild; +export const braintrustEsbuildPlugin = unplugin.esbuild; + +/** + * @deprecated Use {@link braintrustEsbuildPlugin} instead. + */ +export const esbuildPlugin = braintrustEsbuildPlugin; diff --git a/js/src/auto-instrumentations/bundler/rollup.ts b/js/src/auto-instrumentations/bundler/rollup.ts index c89fbff3f..23fab43c8 100644 --- a/js/src/auto-instrumentations/bundler/rollup.ts +++ b/js/src/auto-instrumentations/bundler/rollup.ts @@ -3,10 +3,10 @@ * * Usage: * ```typescript - * import { rollupPlugin } from '@braintrust/auto-instrumentations/bundler/rollup'; + * import { braintrustRollupPlugin } from 'braintrust/rollup'; * * export default { - * plugins: [rollupPlugin()] + * plugins: [braintrustRollupPlugin()] * }; * ``` * @@ -21,4 +21,9 @@ import { unplugin, type BundlerPluginOptions } from "./plugin"; export type RollupPluginOptions = BundlerPluginOptions; -export const rollupPlugin = unplugin.rollup; +export const braintrustRollupPlugin = unplugin.rollup; + +/** + * @deprecated Use {@link braintrustRollupPlugin} instead. + */ +export const rollupPlugin = braintrustRollupPlugin; diff --git a/js/src/auto-instrumentations/bundler/vite.ts b/js/src/auto-instrumentations/bundler/vite.ts index ceb220f9a..1ae4ec8e5 100644 --- a/js/src/auto-instrumentations/bundler/vite.ts +++ b/js/src/auto-instrumentations/bundler/vite.ts @@ -3,10 +3,10 @@ * * Usage: * ```typescript - * import { vitePlugin } from '@braintrust/auto-instrumentations/bundler/vite'; + * import { braintrustVitePlugin } from 'braintrust/vite'; * * export default { - * plugins: [vitePlugin()], + * plugins: [braintrustVitePlugin()], * }; * ``` * @@ -21,4 +21,9 @@ import { unplugin, type BundlerPluginOptions } from "./plugin"; export type VitePluginOptions = BundlerPluginOptions; -export const vitePlugin = unplugin.vite; +export const braintrustVitePlugin = unplugin.vite; + +/** + * @deprecated Use {@link braintrustVitePlugin} instead. + */ +export const vitePlugin = braintrustVitePlugin; diff --git a/js/src/auto-instrumentations/bundler/webpack.ts b/js/src/auto-instrumentations/bundler/webpack.ts index 19497e77c..2918445ba 100644 --- a/js/src/auto-instrumentations/bundler/webpack.ts +++ b/js/src/auto-instrumentations/bundler/webpack.ts @@ -3,10 +3,10 @@ * * Usage: * ```javascript - * import { webpackPlugin } from 'braintrust/auto-instrumentations/bundler/webpack'; + * import { braintrustWebpackPlugin } from 'braintrust/webpack'; * * export default { - * plugins: [webpackPlugin()], + * plugins: [braintrustWebpackPlugin()], * }; * ``` * @@ -21,4 +21,9 @@ import { unplugin, type BundlerPluginOptions } from "./plugin"; export type WebpackPluginOptions = BundlerPluginOptions; -export const webpackPlugin = unplugin.webpack; +export const braintrustWebpackPlugin = unplugin.webpack; + +/** + * @deprecated Use {@link braintrustWebpackPlugin} instead. + */ +export const webpackPlugin = braintrustWebpackPlugin; diff --git a/js/src/auto-instrumentations/index.ts b/js/src/auto-instrumentations/index.ts index 0e3eeb5e6..c88fad706 100644 --- a/js/src/auto-instrumentations/index.ts +++ b/js/src/auto-instrumentations/index.ts @@ -23,8 +23,8 @@ * * **Bundler Plugin (Vite):** * ```typescript - * import { vitePlugin } from '@braintrust/auto-instrumentations/bundler/vite'; - * export default { plugins: [vitePlugin()] }; + * import { braintrustVitePlugin } from 'braintrust/vite'; + * export default { plugins: [braintrustVitePlugin()] }; * ``` */ diff --git a/js/tests/auto-instrumentations/bundler-exports.test.ts b/js/tests/auto-instrumentations/bundler-exports.test.ts new file mode 100644 index 000000000..02cc0b8cf --- /dev/null +++ b/js/tests/auto-instrumentations/bundler-exports.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +describe("bundler plugin exports", () => { + it("exports braintrust-prefixed plugin aliases", async () => { + const esbuild = + await import("../../src/auto-instrumentations/bundler/esbuild.js"); + const rollup = + await import("../../src/auto-instrumentations/bundler/rollup.js"); + const vite = + await import("../../src/auto-instrumentations/bundler/vite.js"); + const webpack = + await import("../../src/auto-instrumentations/bundler/webpack.js"); + + expect(esbuild.braintrustEsbuildPlugin).toBe(esbuild.esbuildPlugin); + expect(rollup.braintrustRollupPlugin).toBe(rollup.rollupPlugin); + expect(vite.braintrustVitePlugin).toBe(vite.vitePlugin); + expect(webpack.braintrustWebpackPlugin).toBe(webpack.webpackPlugin); + }); +}); From 89bb54371582814116eee91ccabec4a7621b8fd3 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 10:43:36 +0200 Subject: [PATCH 2/8] cs --- .changeset/tangy-waves-find.md | 5 +++++ dev-packages/seinfeld/package.json | 1 - dev-packages/seinfeld/src/cassette.ts | 2 -- dev-packages/seinfeld/src/recorder.ts | 8 +------- dev-packages/seinfeld/tsup.config.ts | 6 ------ e2e/package.json | 1 - js/src/wrappers/claude-agent-sdk/package.json | 1 - js/src/wrappers/vitest/package.json | 1 - 8 files changed, 6 insertions(+), 19 deletions(-) create mode 100644 .changeset/tangy-waves-find.md diff --git a/.changeset/tangy-waves-find.md b/.changeset/tangy-waves-find.md new file mode 100644 index 000000000..629d1cca4 --- /dev/null +++ b/.changeset/tangy-waves-find.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat(bundler-plugins): Add `braintrust{Bundler}Plugin` aliases for bundler plugins and deprecate rest diff --git a/dev-packages/seinfeld/package.json b/dev-packages/seinfeld/package.json index 541b2c168..98d8fea96 100644 --- a/dev-packages/seinfeld/package.json +++ b/dev-packages/seinfeld/package.json @@ -1,6 +1,5 @@ { "name": "@braintrust/seinfeld", - "version": "0.0.0", "private": true, "description": "Internal cassette server for e2e provider tests.", "type": "module", diff --git a/dev-packages/seinfeld/src/cassette.ts b/dev-packages/seinfeld/src/cassette.ts index f31e05c23..47d032ec2 100644 --- a/dev-packages/seinfeld/src/cassette.ts +++ b/dev-packages/seinfeld/src/cassette.ts @@ -75,8 +75,6 @@ export interface CassetteEntry { export interface CassetteMeta { /** ISO-8601 timestamp when the cassette was first created. */ createdAt: string; - /** The seinfeld version that produced the cassette. */ - seinfeldVersion: string; } /** diff --git a/dev-packages/seinfeld/src/recorder.ts b/dev-packages/seinfeld/src/recorder.ts index dbd0bbf83..24665355b 100644 --- a/dev-packages/seinfeld/src/recorder.ts +++ b/dev-packages/seinfeld/src/recorder.ts @@ -39,12 +39,6 @@ import { import { encodeBinaryDraft, encodeBody } from "./serializer"; import type { CassetteStore } from "./store"; -// Injected at build time by tsup define. Falls back to 'dev' when running -// directly without a build step. -declare const __SEINFELD_VERSION__: string; -const SEINFELD_VERSION: string = - typeof __SEINFELD_VERSION__ !== "undefined" ? __SEINFELD_VERSION__ : "dev"; - const DEFAULT_EXTERNAL_BLOB_THRESHOLD = 65536; export type RequestUrlMatcher = string | RegExp; @@ -369,7 +363,7 @@ async function persistIfRecord(ctx: CassetteContext): Promise { } const cassette: CassetteFile = { version: CURRENT_FORMAT_VERSION, - meta: { createdAt, seinfeldVersion: SEINFELD_VERSION }, + meta: { createdAt }, entries: flushedEntries, }; await ctx.store.save(ctx.name, cassette); diff --git a/dev-packages/seinfeld/tsup.config.ts b/dev-packages/seinfeld/tsup.config.ts index edf0a9be2..41c6c169c 100644 --- a/dev-packages/seinfeld/tsup.config.ts +++ b/dev-packages/seinfeld/tsup.config.ts @@ -1,5 +1,4 @@ import { defineConfig } from "tsup"; -import pkg from "./package.json"; export default defineConfig({ entry: { @@ -12,9 +11,4 @@ export default defineConfig({ target: "node18", splitting: false, treeshake: true, - // Inject the package version at build time so the cassette meta field - // always matches the installed library version. - define: { - __SEINFELD_VERSION__: JSON.stringify(pkg.version), - }, }); diff --git a/e2e/package.json b/e2e/package.json index 3bc0a27d5..05169aee6 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,5 @@ { "name": "@braintrust/js-e2e-tests", - "version": "0.0.0", "private": true, "type": "module", "scripts": { diff --git a/js/src/wrappers/claude-agent-sdk/package.json b/js/src/wrappers/claude-agent-sdk/package.json index 2680cf816..f33503013 100644 --- a/js/src/wrappers/claude-agent-sdk/package.json +++ b/js/src/wrappers/claude-agent-sdk/package.json @@ -1,6 +1,5 @@ { "name": "@braintrust/claude-agent-sdk-tests", - "version": "0.0.1", "private": true, "description": "Test infrastructure for Claude Agent SDK wrapper", "scripts": { diff --git a/js/src/wrappers/vitest/package.json b/js/src/wrappers/vitest/package.json index 418297fba..f3fdf0fd4 100644 --- a/js/src/wrappers/vitest/package.json +++ b/js/src/wrappers/vitest/package.json @@ -1,6 +1,5 @@ { "name": "@braintrust/vitest-wrapper-tests", - "version": "0.0.1", "private": true, "description": "Test infrastructure for Vitest wrapper", "scripts": { From 3b7fbe0d6993bb62b519416b9fc5d29ac025e1be Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 16:57:26 +0200 Subject: [PATCH 3/8] clean --- js/src/auto-instrumentations/README.md | 25 +- .../auto-instrumentations/bundler/esbuild.ts | 45 ++-- .../auto-instrumentations/bundler/plugin.ts | 249 +++++++++--------- .../auto-instrumentations/bundler/rollup.ts | 45 ++-- js/src/auto-instrumentations/bundler/vite.ts | 45 ++-- .../auto-instrumentations/bundler/webpack.ts | 45 ++-- .../bundler-exports.test.ts | 19 -- .../transformation.test.ts | 53 ++-- 8 files changed, 250 insertions(+), 276 deletions(-) delete mode 100644 js/tests/auto-instrumentations/bundler-exports.test.ts diff --git a/js/src/auto-instrumentations/README.md b/js/src/auto-instrumentations/README.md index 83062bea1..c1f5d7360 100644 --- a/js/src/auto-instrumentations/README.md +++ b/js/src/auto-instrumentations/README.md @@ -248,13 +248,11 @@ When using bundler plugins (Vite, Webpack, etc.) in Node.js: import { braintrustVitePlugin } from "braintrust/vite"; export default { - plugins: [ - braintrustVitePlugin({ browser: false }), // IMPORTANT: Set browser: false for Node.js - ], + plugins: [braintrustVitePlugin()], }; ``` -Setting `browser: false` ensures the code-transformer injects `node:diagnostics_channel` imports, not `dc-browser`. +Braintrust-prefixed bundler plugins use Node.js's native `node:diagnostics_channel` module by default. ### Browser Setup @@ -265,19 +263,18 @@ Setting `browser: false` ensures the code-transformer injects `node:diagnostics_ import { braintrustVitePlugin } from "braintrust/vite"; export default { - plugins: [ - braintrustVitePlugin({ browser: true }), // Use browser: true for browser builds - ], + plugins: [braintrustVitePlugin({ useDiagnosticChannelCompatShim: true })], }; ``` -Setting `browser: true` ensures the code-transformer injects `dc-browser` imports. +Setting `useDiagnosticChannelCompatShim: true` ensures the code-transformer injects `dc-browser` imports for environments where Node.js's `diagnostics_channel` module is unavailable. +The deprecated plugin exports are still available and preserve the old `browser` option, which defaults to `true` when omitted; the new Braintrust-prefixed option defaults to `false`. -**Important:** The `browser` option must match your target environment: +**Important:** The `useDiagnosticChannelCompatShim` option must match your target environment: -- Mismatch (e.g., `browser: true` but running in Node.js) causes channel registry conflicts +- Mismatch (e.g., enabling the compatibility shim but running in Node.js) causes channel registry conflicts - Plugin code uses the iso pattern and adapts automatically -- Only the transformed SDK code is affected by the `browser` option +- Only the transformed SDK code is affected by the compatibility shim option ## Advanced: Custom Plugins @@ -362,7 +359,7 @@ pnpm test -- tests/auto-instrumentations/integration.test.ts #### 2. Browser/Node.js Mismatch -**Problem:** Bundler `browser` option doesn't match runtime environment. +**Problem:** Bundler diagnostics channel compatibility setting doesn't match runtime environment. **Symptoms:** @@ -371,8 +368,8 @@ pnpm test -- tests/auto-instrumentations/integration.test.ts **Solution:** -- For Node.js apps: Use `browser: false` in bundler config -- For browser apps: Use `browser: true` in bundler config +- For Node.js apps: Use a Braintrust-prefixed bundler plugin with default options +- For browser apps: Set `useDiagnosticChannelCompatShim: true` - For Node.js runtime apps: Use the loader hook instead of bundling #### 3. APIPromise Compatibility (Anthropic SDK) diff --git a/js/src/auto-instrumentations/bundler/esbuild.ts b/js/src/auto-instrumentations/bundler/esbuild.ts index c63c1d631..8c2576302 100644 --- a/js/src/auto-instrumentations/bundler/esbuild.ts +++ b/js/src/auto-instrumentations/bundler/esbuild.ts @@ -1,29 +1,26 @@ -/** - * esbuild plugin for auto-instrumentation. - * - * Usage: - * ```typescript - * import { braintrustEsbuildPlugin } from 'braintrust/esbuild'; - * - * await esbuild.build({ - * plugins: [braintrustEsbuildPlugin()], - * }); - * ``` - * - * This plugin uses @apm-js-collab/code-transformer to perform AST transformation - * at build-time, injecting TracingChannel calls into AI SDK functions. - * - * For browser builds, the plugin automatically uses 'dc-browser' for diagnostics_channel polyfill. - * The als-browser polyfill for AsyncLocalStorage is automatically included as a dependency. - */ - -import { unplugin, type BundlerPluginOptions } from "./plugin"; +import type { EsbuildPlugin } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -export type EsbuildPluginOptions = BundlerPluginOptions; +export function braintrustEsbuildPlugin( + options: BundlerPluginOptions = {}, +): EsbuildPlugin { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.esbuild({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export const braintrustEsbuildPlugin = unplugin.esbuild; +export type EsbuildPluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustEsbuildPlugin} instead. + * @deprecated Use {@link braintrustEsbuildPlugin} instead. This legacy export + * defaults to browser-compatible diagnostics channel shimming when `browser` + * is omitted; `braintrustEsbuildPlugin` defaults to Node.js diagnostics_channel + * unless `useDiagnosticChannelCompatShim` is set to `true`. */ -export const esbuildPlugin = braintrustEsbuildPlugin; +export const esbuildPlugin = unplugin.esbuild; diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 267224ac4..3d84e8b91 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -1,16 +1,3 @@ -/** - * Shared plugin implementation for auto-instrumentation across bundlers. - * - * This module contains the common logic used by all bundler-specific plugins - * (webpack, rollup, esbuild, vite). Each bundler exports a thin wrapper that - * uses this shared implementation. - * - * This plugin uses @apm-js-collab/code-transformer to perform AST transformation - * at build-time, injecting TracingChannel calls into AI SDK functions. - * - * For browser builds, the plugin automatically uses 'dc-browser' for diagnostics_channel polyfill. - */ - import { createUnplugin } from "unplugin"; import { create, @@ -36,7 +23,7 @@ import { groqConfigs } from "../configs/groq"; import { genkitConfigs } from "../configs/genkit"; import { gitHubCopilotConfigs } from "../configs/github-copilot"; -export interface BundlerPluginOptions { +export interface LegacyBundlerPluginOptions { /** * Enable debug logging */ @@ -57,6 +44,26 @@ export interface BundlerPluginOptions { browser?: boolean; } +export interface BundlerPluginOptions { + /** + * Enable debug logging + */ + debug?: boolean; + + /** + * Use the `diagnostics_channel` compatibility shim in patched code instead + * of Node.js's built-in `diagnostics_channel` module. + * + * Enable this for browser, edge, or worker bundles where Node's + * `diagnostics_channel` module is unavailable. Leave it disabled for Node.js + * bundles so transformed SDK code publishes on the native `diagnostics_channel` + * registry. + * + * @default false + */ + useDiagnosticChannelCompatShim?: boolean; +} + /** * Helper function to get module version from package.json */ @@ -74,111 +81,113 @@ function getModuleVersion(basedir: string): string | undefined { return undefined; // No version found } -export const unplugin = createUnplugin((options = {}) => { - const allInstrumentations = [ - ...openaiConfigs, - ...openAICodexConfigs, - ...anthropicConfigs, - ...aiSDKConfigs, - ...claudeAgentSDKConfigs, - ...cursorSDKConfigs, - ...googleGenAIConfigs, - ...huggingFaceConfigs, - ...openRouterConfigs, - ...openRouterAgentConfigs, - ...mistralConfigs, - ...cohereConfigs, - ...groqConfigs, - ...genkitConfigs, - ...gitHubCopilotConfigs, - ...(options.instrumentations || []), - ]; - - // Default to browser build, use polyfill unless explicitly disabled - const dcModule = options.browser === false ? undefined : "dc-browser"; - - // Create the code transformer instrumentor - const instrumentationMatcher = create(allInstrumentations, dcModule); - - return { - name: "code-transformer", - enforce: "pre", - transform(code: string, id: string) { - if (!id) { - // Some modules apparently don't have an id? - return null; - } - - // Convert file:// URLs to regular paths at entry point - // Node.js ESM loader hooks provide file:// URLs, but downstream code expects paths - const filePath = id.startsWith("file:") ? fileURLToPath(id) : id; - - // Determine if this is an ES module using multiple methods for accurate detection - const ext = extname(filePath); - let isModule = ext === ".mjs" || ext === ".ts" || ext === ".tsx"; - - // For .js files, use content analysis for module detection - if (ext === ".js") { - isModule = code.includes("export ") || code.includes("import "); - } - - // Try to get module details from the file path - // IMPORTANT: module-details-from-path uses path.sep to split paths. - // On Windows (path.sep = '\'), we need to convert forward slashes to backslashes. - // On Unix (path.sep = '/'), paths should already use forward slashes. - // Some bundlers (like Vite/Rollup) may pass paths with forward slashes even on Windows. - const normalizedForPlatform = filePath.split("/").join(sep); - const moduleDetails = moduleDetailsFromPath(normalizedForPlatform); - - // If no module details found, the file is not part of a module - if (!moduleDetails) { - return null; - } - - // Use module details for accurate module information - const moduleName = moduleDetails.name; - const moduleVersion = getModuleVersion(moduleDetails.basedir); - - // If no version found - if (!moduleVersion) { - console.warn( - `No 'package.json' version found for module ${moduleName} at ${moduleDetails.basedir}. Skipping transformation.`, - ); - return null; - } - - // Try to get a transformer for this file - // Normalize the module path for Windows compatibility (WASM transformer expects forward slashes) - const normalizedModulePath = moduleDetails.path.replace(/\\/g, "/"); - const transformer = instrumentationMatcher.getTransformer( - moduleName, - moduleVersion, - normalizedModulePath, - ); - - if (!transformer) { - // No instrumentations match this file - return null; - } - - try { - // Transform the code - const moduleType = isModule ? "esm" : "cjs"; - const result = transformer.transform(code, moduleType); - const transformedCode = result.code.replace( - /const \{tracingChannel: ([A-Za-z_$][\w$]*)\} = ([A-Za-z_$][\w$]*);/g, - "const $1 = $2.tracingChannel;", +export const unplugin = createUnplugin( + (options = {}) => { + const allInstrumentations = [ + ...openaiConfigs, + ...openAICodexConfigs, + ...anthropicConfigs, + ...aiSDKConfigs, + ...claudeAgentSDKConfigs, + ...cursorSDKConfigs, + ...googleGenAIConfigs, + ...huggingFaceConfigs, + ...openRouterConfigs, + ...openRouterAgentConfigs, + ...mistralConfigs, + ...cohereConfigs, + ...groqConfigs, + ...genkitConfigs, + ...gitHubCopilotConfigs, + ...(options.instrumentations || []), + ]; + + // Default to browser build, use polyfill unless explicitly disabled + const dcModule = options.browser === false ? undefined : "dc-browser"; + + // Create the code transformer instrumentor + const instrumentationMatcher = create(allInstrumentations, dcModule); + + return { + name: "code-transformer", + enforce: "pre", + transform(code: string, id: string) { + if (!id) { + // Some modules apparently don't have an id? + return null; + } + + // Convert file:// URLs to regular paths at entry point + // Node.js ESM loader hooks provide file:// URLs, but downstream code expects paths + const filePath = id.startsWith("file:") ? fileURLToPath(id) : id; + + // Determine if this is an ES module using multiple methods for accurate detection + const ext = extname(filePath); + let isModule = ext === ".mjs" || ext === ".ts" || ext === ".tsx"; + + // For .js files, use content analysis for module detection + if (ext === ".js") { + isModule = code.includes("export ") || code.includes("import "); + } + + // Try to get module details from the file path + // IMPORTANT: module-details-from-path uses path.sep to split paths. + // On Windows (path.sep = '\'), we need to convert forward slashes to backslashes. + // On Unix (path.sep = '/'), paths should already use forward slashes. + // Some bundlers (like Vite/Rollup) may pass paths with forward slashes even on Windows. + const normalizedForPlatform = filePath.split("/").join(sep); + const moduleDetails = moduleDetailsFromPath(normalizedForPlatform); + + // If no module details found, the file is not part of a module + if (!moduleDetails) { + return null; + } + + // Use module details for accurate module information + const moduleName = moduleDetails.name; + const moduleVersion = getModuleVersion(moduleDetails.basedir); + + // If no version found + if (!moduleVersion) { + console.warn( + `No 'package.json' version found for module ${moduleName} at ${moduleDetails.basedir}. Skipping transformation.`, + ); + return null; + } + + // Try to get a transformer for this file + // Normalize the module path for Windows compatibility (WASM transformer expects forward slashes) + const normalizedModulePath = moduleDetails.path.replace(/\\/g, "/"); + const transformer = instrumentationMatcher.getTransformer( + moduleName, + moduleVersion, + normalizedModulePath, ); - return { - code: transformedCode, - map: result.map, - }; - } catch (error) { - // If transformation fails, warn and return original code - console.warn(`Code transformation failed for ${id}: ${error}`); - return null; - } - }, - }; -}); + if (!transformer) { + // No instrumentations match this file + return null; + } + + try { + // Transform the code + const moduleType = isModule ? "esm" : "cjs"; + const result = transformer.transform(code, moduleType); + const transformedCode = result.code.replace( + /const \{tracingChannel: ([A-Za-z_$][\w$]*)\} = ([A-Za-z_$][\w$]*);/g, + "const $1 = $2.tracingChannel;", + ); + + return { + code: transformedCode, + map: result.map, + }; + } catch (error) { + // If transformation fails, warn and return original code + console.warn(`Code transformation failed for ${id}: ${error}`); + return null; + } + }, + }; + }, +); diff --git a/js/src/auto-instrumentations/bundler/rollup.ts b/js/src/auto-instrumentations/bundler/rollup.ts index 23fab43c8..3f9d2a9a1 100644 --- a/js/src/auto-instrumentations/bundler/rollup.ts +++ b/js/src/auto-instrumentations/bundler/rollup.ts @@ -1,29 +1,26 @@ -/** - * Rollup plugin for auto-instrumentation. - * - * Usage: - * ```typescript - * import { braintrustRollupPlugin } from 'braintrust/rollup'; - * - * export default { - * plugins: [braintrustRollupPlugin()] - * }; - * ``` - * - * This plugin uses @apm-js-collab/code-transformer to perform AST transformation - * at build-time, injecting TracingChannel calls into AI SDK functions. - * - * For browser builds, the plugin automatically uses 'dc-browser' for diagnostics_channel polyfill. - * The als-browser polyfill for AsyncLocalStorage is automatically included as a dependency. - */ - -import { unplugin, type BundlerPluginOptions } from "./plugin"; +import type { RollupPlugin } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -export type RollupPluginOptions = BundlerPluginOptions; +export function braintrustRollupPlugin( + options: BundlerPluginOptions = {}, +): RollupPlugin | RollupPlugin[] { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.rollup({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export const braintrustRollupPlugin = unplugin.rollup; +export type RollupPluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustRollupPlugin} instead. + * @deprecated Use {@link braintrustRollupPlugin} instead. This legacy export + * defaults to browser-compatible diagnostics channel shimming when `browser` + * is omitted; `braintrustRollupPlugin` defaults to Node.js diagnostics_channel + * unless `useDiagnosticChannelCompatShim` is set to `true`. */ -export const rollupPlugin = braintrustRollupPlugin; +export const rollupPlugin = unplugin.rollup; diff --git a/js/src/auto-instrumentations/bundler/vite.ts b/js/src/auto-instrumentations/bundler/vite.ts index 1ae4ec8e5..004cc53ea 100644 --- a/js/src/auto-instrumentations/bundler/vite.ts +++ b/js/src/auto-instrumentations/bundler/vite.ts @@ -1,29 +1,26 @@ -/** - * Vite plugin for auto-instrumentation. - * - * Usage: - * ```typescript - * import { braintrustVitePlugin } from 'braintrust/vite'; - * - * export default { - * plugins: [braintrustVitePlugin()], - * }; - * ``` - * - * This plugin uses @apm-js-collab/code-transformer to perform AST transformation - * at build-time, injecting TracingChannel calls into AI SDK functions. - * - * For browser builds, the plugin automatically uses 'dc-browser' for diagnostics_channel polyfill. - * The als-browser polyfill for AsyncLocalStorage is automatically included as a dependency. - */ - -import { unplugin, type BundlerPluginOptions } from "./plugin"; +import type { VitePlugin } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -export type VitePluginOptions = BundlerPluginOptions; +export function braintrustVitePlugin( + options: BundlerPluginOptions = {}, +): VitePlugin | VitePlugin[] { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.vite({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export const braintrustVitePlugin = unplugin.vite; +export type VitePluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustVitePlugin} instead. + * @deprecated Use {@link braintrustVitePlugin} instead. This legacy export + * defaults to browser-compatible diagnostics channel shimming when `browser` + * is omitted; `braintrustVitePlugin` defaults to Node.js diagnostics_channel + * unless `useDiagnosticChannelCompatShim` is set to `true`. */ -export const vitePlugin = braintrustVitePlugin; +export const vitePlugin = unplugin.vite; diff --git a/js/src/auto-instrumentations/bundler/webpack.ts b/js/src/auto-instrumentations/bundler/webpack.ts index 2918445ba..23c1e237d 100644 --- a/js/src/auto-instrumentations/bundler/webpack.ts +++ b/js/src/auto-instrumentations/bundler/webpack.ts @@ -1,29 +1,26 @@ -/** - * Webpack plugin for auto-instrumentation. - * - * Usage: - * ```javascript - * import { braintrustWebpackPlugin } from 'braintrust/webpack'; - * - * export default { - * plugins: [braintrustWebpackPlugin()], - * }; - * ``` - * - * This plugin uses @apm-js-collab/code-transformer to perform AST transformation - * at build-time, injecting TracingChannel calls into AI SDK functions. - * - * For browser builds, the plugin automatically uses 'dc-browser' for diagnostics_channel polyfill. - * The als-browser polyfill for AsyncLocalStorage is automatically included as a dependency. - */ - -import { unplugin, type BundlerPluginOptions } from "./plugin"; +import type { WebpackPluginInstance } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -export type WebpackPluginOptions = BundlerPluginOptions; +export function braintrustWebpackPlugin( + options: BundlerPluginOptions = {}, +): WebpackPluginInstance { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.webpack({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export const braintrustWebpackPlugin = unplugin.webpack; +export type WebpackPluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustWebpackPlugin} instead. + * @deprecated Use {@link braintrustWebpackPlugin} instead. This legacy export + * defaults to browser-compatible diagnostics channel shimming when `browser` + * is omitted; `braintrustWebpackPlugin` defaults to Node.js diagnostics_channel + * unless `useDiagnosticChannelCompatShim` is set to `true`. */ -export const webpackPlugin = braintrustWebpackPlugin; +export const webpackPlugin = unplugin.webpack; diff --git a/js/tests/auto-instrumentations/bundler-exports.test.ts b/js/tests/auto-instrumentations/bundler-exports.test.ts deleted file mode 100644 index 02cc0b8cf..000000000 --- a/js/tests/auto-instrumentations/bundler-exports.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from "vitest"; - -describe("bundler plugin exports", () => { - it("exports braintrust-prefixed plugin aliases", async () => { - const esbuild = - await import("../../src/auto-instrumentations/bundler/esbuild.js"); - const rollup = - await import("../../src/auto-instrumentations/bundler/rollup.js"); - const vite = - await import("../../src/auto-instrumentations/bundler/vite.js"); - const webpack = - await import("../../src/auto-instrumentations/bundler/webpack.js"); - - expect(esbuild.braintrustEsbuildPlugin).toBe(esbuild.esbuildPlugin); - expect(rollup.braintrustRollupPlugin).toBe(rollup.rollupPlugin); - expect(vite.braintrustVitePlugin).toBe(vite.vitePlugin); - expect(webpack.braintrustWebpackPlugin).toBe(webpack.webpackPlugin); - }); -}); diff --git a/js/tests/auto-instrumentations/transformation.test.ts b/js/tests/auto-instrumentations/transformation.test.ts index c51f12758..77eb34b16 100644 --- a/js/tests/auto-instrumentations/transformation.test.ts +++ b/js/tests/auto-instrumentations/transformation.test.ts @@ -39,7 +39,7 @@ describe("Orchestrion Transformation Tests", () => { describe("esbuild", () => { it("should transform OpenAI SDK code with tracingChannel", async () => { - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); const entryPoint = path.join(fixturesDir, "test-app.js"); @@ -51,9 +51,7 @@ describe("Orchestrion Transformation Tests", () => { write: true, outfile, format: "esm", - plugins: [ - esbuildPlugin({ browser: false }), // Use Node.js built-in for tests - ], + plugins: [braintrustEsbuildPlugin()], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, // CRITICAL: Don't dereference symlinks! @@ -68,10 +66,11 @@ describe("Orchestrion Transformation Tests", () => { // Verify orchestrion transformed the code expect(output).toContain("tracingChannel"); expect(output).toContain("orchestrion:openai:chat.completions.create"); + expect(output).not.toContain("TracingChannel"); }); - it("should bundle dc-browser module when browser: true", async () => { - const { esbuildPlugin } = + it("should bundle dc-browser module when useDiagnosticChannelCompatShim is true", async () => { + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); const entryPoint = path.join(fixturesDir, "test-app.js"); @@ -84,7 +83,7 @@ describe("Orchestrion Transformation Tests", () => { outfile, format: "esm", plugins: [ - esbuildPlugin({ browser: true }), // Use browser mode + braintrustEsbuildPlugin({ useDiagnosticChannelCompatShim: true }), ], logLevel: "error", absWorkingDir: fixturesDir, @@ -110,7 +109,7 @@ describe("Orchestrion Transformation Tests", () => { describe("vite", () => { it("should transform OpenAI SDK code with tracingChannel", async () => { - const { vitePlugin } = + const { braintrustVitePlugin } = await import("../../src/auto-instrumentations/bundler/vite.js"); const entryPoint = path.join(fixturesDir, "test-app.js"); @@ -131,9 +130,7 @@ describe("Orchestrion Transformation Tests", () => { external: ["diagnostics_channel"], // Mark Node built-ins as external, don't try to bundle them }, }, - plugins: [ - vitePlugin({ browser: false }), // Use Node.js built-in for tests - ], + plugins: [braintrustVitePlugin()], logLevel: "error", resolve: { preserveSymlinks: true, // Don't dereference symlinks @@ -148,10 +145,11 @@ describe("Orchestrion Transformation Tests", () => { // Verify orchestrion transformed the code expect(output).toContain("tracingChannel"); expect(output).toContain("orchestrion:openai:chat.completions.create"); + expect(output).not.toContain("TracingChannel"); }); - it("should bundle dc-browser module when browser: true", async () => { - const { vitePlugin } = + it("should bundle dc-browser module when useDiagnosticChannelCompatShim is true", async () => { + const { braintrustVitePlugin } = await import("../../src/auto-instrumentations/bundler/vite.js"); const entryPoint = path.join(fixturesDir, "test-app.js"); @@ -170,7 +168,7 @@ describe("Orchestrion Transformation Tests", () => { minify: false, }, plugins: [ - vitePlugin({ browser: true }), // Use browser mode + braintrustVitePlugin({ useDiagnosticChannelCompatShim: true }), ], logLevel: "error", resolve: { @@ -222,7 +220,7 @@ describe("Orchestrion Transformation Tests", () => { } it("should transform OpenAI SDK code with tracingChannel", async () => { - const { webpackPlugin } = + const { braintrustWebpackPlugin } = await import("../../src/auto-instrumentations/bundler/webpack.js"); const { errors, output } = await runWebpack({ @@ -236,16 +234,17 @@ describe("Orchestrion Transformation Tests", () => { mode: "development", resolve: { modules: [nodeModulesDir, "node_modules"] }, externals: { diagnostics_channel: "module diagnostics_channel" }, - plugins: [webpackPlugin({ browser: false })], + plugins: [braintrustWebpackPlugin()], }); expect(errors).toHaveLength(0); expect(output).toContain("tracingChannel"); expect(output).toContain("orchestrion:openai:chat.completions.create"); + expect(output).not.toContain("TracingChannel"); }); - it("should bundle dc-browser module when browser: true", async () => { - const { webpackPlugin } = + it("should bundle dc-browser module when useDiagnosticChannelCompatShim is true", async () => { + const { braintrustWebpackPlugin } = await import("../../src/auto-instrumentations/bundler/webpack.js"); const { errors, output } = await runWebpack({ @@ -258,7 +257,9 @@ describe("Orchestrion Transformation Tests", () => { experiments: { outputModule: true }, mode: "development", resolve: { modules: [nodeModulesDir, "node_modules"] }, - plugins: [webpackPlugin({ browser: true })], + plugins: [ + braintrustWebpackPlugin({ useDiagnosticChannelCompatShim: true }), + ], }); expect(errors).toHaveLength(0); @@ -370,7 +371,7 @@ describe("Orchestrion Transformation Tests", () => { describe("rollup", () => { it("should transform OpenAI SDK code with tracingChannel", async () => { const { rollup } = await import("rollup"); - const { rollupPlugin } = + const { braintrustRollupPlugin } = await import("../../src/auto-instrumentations/bundler/rollup.js"); const entryPoint = path.join(fixturesDir, "test-app.js"); @@ -392,10 +393,7 @@ describe("Orchestrion Transformation Tests", () => { const bundle = await rollup({ input: entryPoint, - plugins: [ - resolverPlugin, - rollupPlugin({ browser: false }), // Use Node.js built-in for tests - ], + plugins: [resolverPlugin, braintrustRollupPlugin()], external: [], preserveSymlinks: true, // Don't dereference symlinks }); @@ -414,11 +412,12 @@ describe("Orchestrion Transformation Tests", () => { // Verify orchestrion transformed the code expect(output).toContain("tracingChannel"); expect(output).toContain("orchestrion:openai:chat.completions.create"); + expect(output).not.toContain("TracingChannel"); }); - it("should bundle dc-browser module when browser: true", async () => { + it("should bundle dc-browser module when useDiagnosticChannelCompatShim is true", async () => { const { rollup } = await import("rollup"); - const { rollupPlugin } = + const { braintrustRollupPlugin } = await import("../../src/auto-instrumentations/bundler/rollup.js"); const entryPoint = path.join(fixturesDir, "test-app.js"); @@ -451,7 +450,7 @@ describe("Orchestrion Transformation Tests", () => { input: entryPoint, plugins: [ resolverPlugin, - rollupPlugin({ browser: true }), // Use browser mode + braintrustRollupPlugin({ useDiagnosticChannelCompatShim: true }), ], external: [], preserveSymlinks: true, From 596869049461ccceedc7a607be10d00b928d3491 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 17:08:13 +0200 Subject: [PATCH 4/8] fix --- .../auto-instrumentations/bundler/webpack-loader.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 97162162d..6b00f7d6e 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -43,7 +43,10 @@ import { cohereConfigs } from "../configs/cohere"; import { groqConfigs } from "../configs/groq"; import { genkitConfigs } from "../configs/genkit"; import { gitHubCopilotConfigs } from "../configs/github-copilot"; -import { type BundlerPluginOptions } from "./plugin"; +import { + LegacyBundlerPluginOptions, + type BundlerPluginOptions, +} from "./plugin"; /** * Helper function to get module version from package.json @@ -68,7 +71,9 @@ const matcherCache = new Map(); /** * Get or create a matcher instance, caching by config hash */ -function getMatcher(options: BundlerPluginOptions): InstrumentationMatcher { +function getMatcher( + options: LegacyBundlerPluginOptions, +): InstrumentationMatcher { const allInstrumentations = [ ...openaiConfigs, ...anthropicConfigs, @@ -190,7 +195,7 @@ function codeTransformerLoader( // Attach Options type to the loader function namespace codeTransformerLoader { - export type Options = BundlerPluginOptions; + export type Options = LegacyBundlerPluginOptions; } export = codeTransformerLoader; From cad5d4af4fe8145b759a731b89299c3e698fe846 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 17:33:43 +0200 Subject: [PATCH 5/8] cassette version rm --- dev-packages/seinfeld/src/cassette.ts | 1 - dev-packages/seinfeld/src/errors.ts | 27 -------- dev-packages/seinfeld/src/format/index.ts | 65 ++----------------- dev-packages/seinfeld/src/format/v1.ts | 16 +---- dev-packages/seinfeld/src/store/file-store.ts | 2 - 5 files changed, 9 insertions(+), 102 deletions(-) diff --git a/dev-packages/seinfeld/src/cassette.ts b/dev-packages/seinfeld/src/cassette.ts index 47d032ec2..2ca69b387 100644 --- a/dev-packages/seinfeld/src/cassette.ts +++ b/dev-packages/seinfeld/src/cassette.ts @@ -85,7 +85,6 @@ export interface CassetteMeta { * incompatibly. */ export interface CassetteFile { - version: 1; meta?: CassetteMeta; entries: CassetteEntry[]; } diff --git a/dev-packages/seinfeld/src/errors.ts b/dev-packages/seinfeld/src/errors.ts index cacb95fa2..325307fe4 100644 --- a/dev-packages/seinfeld/src/errors.ts +++ b/dev-packages/seinfeld/src/errors.ts @@ -28,33 +28,6 @@ export class CassetteMissError extends Error { } } -/** - * Thrown when a cassette file's `version` field is newer than this library - * supports. Catching this and pointing the user at an upgrade is more useful - * than silently downgrading. - */ -export class CassetteVersionError extends Error { - readonly cassetteName: string; - readonly foundVersion: number; - readonly supportedVersion: number; - - constructor(args: { - cassetteName: string; - foundVersion: number; - supportedVersion: number; - }) { - super( - `Cassette "${args.cassetteName}" has version ${args.foundVersion}, ` + - `but this version of seinfeld supports up to version ${args.supportedVersion}. ` + - `Upgrade seinfeld to read this cassette.`, - ); - this.name = "CassetteVersionError"; - this.cassetteName = args.cassetteName; - this.foundVersion = args.foundVersion; - this.supportedVersion = args.supportedVersion; - } -} - /** Thrown when a cassette file fails schema validation. */ export class CassetteFormatError extends Error { readonly cassetteName: string; diff --git a/dev-packages/seinfeld/src/format/index.ts b/dev-packages/seinfeld/src/format/index.ts index 28900b6d8..1698eb880 100644 --- a/dev-packages/seinfeld/src/format/index.ts +++ b/dev-packages/seinfeld/src/format/index.ts @@ -1,72 +1,21 @@ -/** - * Versioned cassette format dispatcher. - * - * `parseCassette` reads the `version` field and routes to the appropriate - * schema. Unknown fields at entry level are preserved via `.passthrough()` in - * each version schema so minor additions within a major version survive - * round-trips. - * - * Rule for bumping versions: - * - New optional fields in an existing version: add to the schema with - * `.optional()`; no version bump needed (passthrough preserves them for - * older readers too). - * - Breaking / required changes: add a `v2.ts` schema, add a migration in - * `migrateV1ToV2`, and bump `CURRENT_FORMAT_VERSION` there. - */ - import type { CassetteFile } from "../cassette"; -import { CassetteFormatError, CassetteVersionError } from "../errors"; -import { CURRENT_FORMAT_VERSION, cassetteSchema } from "./v1"; - -export { CURRENT_FORMAT_VERSION } from "./v1"; +import { CassetteFormatError } from "../errors"; +import { cassetteSchema } from "./v1"; /** * Parse a raw (JSON-deserialized) cassette object, dispatching to the correct - * version schema. Throws `CassetteVersionError` for unsupported versions and - * `CassetteFormatError` for schema mismatches. + * version schema. Throws `CassetteFormatError` for schema mismatches. */ export function parseCassette( raw: unknown, cassetteName: string, ): CassetteFile { - if ( - typeof raw !== "object" || - raw === null || - !("version" in raw) || - typeof raw.version !== "number" - ) { + const result = cassetteSchema.safeParse(raw); + if (!result.success) { throw new CassetteFormatError({ cassetteName, - message: 'Missing or invalid "version" field', - }); - } - - const version = (raw as { version: number }).version; - - if (version > CURRENT_FORMAT_VERSION) { - throw new CassetteVersionError({ - cassetteName, - foundVersion: version, - supportedVersion: CURRENT_FORMAT_VERSION, + message: result.error.message, }); } - - // Route to version-specific schema. When v2 is added, add another branch. - if (version === 1) { - const result = cassetteSchema.safeParse(raw); - if (!result.success) { - throw new CassetteFormatError({ - cassetteName, - message: result.error.message, - }); - } - return result.data; - } - - // version < 1 — too old, no migration available - throw new CassetteVersionError({ - cassetteName, - foundVersion: version, - supportedVersion: CURRENT_FORMAT_VERSION, - }); + return result.data; } diff --git a/dev-packages/seinfeld/src/format/v1.ts b/dev-packages/seinfeld/src/format/v1.ts index 43b768d95..558ddcfb5 100644 --- a/dev-packages/seinfeld/src/format/v1.ts +++ b/dev-packages/seinfeld/src/format/v1.ts @@ -1,17 +1,5 @@ import { z } from "zod"; -/** - * Zod schema for cassette format version 1. - * - * Cassette files carry a `version` field so that: (a) load-time validation - * fails loudly on corrupt files rather than silently at match time, and - * (b) a future breaking change can introduce v2 while the library still reads - * older cassettes without ambiguity. See `format/index.ts` for the dispatch - * logic and the rule for when to bump the version. - */ - -export const CURRENT_FORMAT_VERSION = 1 as const; - const bodyPayloadSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("empty") }), z.object({ kind: z.literal("json"), value: z.unknown() }), @@ -61,12 +49,12 @@ const cassetteEntrySchema = z const cassetteMetaSchema = z .object({ createdAt: z.string(), - seinfeldVersion: z.string(), + seinfeldVersion: z.string().optional(), // TODO(luca): Remove after cassettes no longer have this field }) .passthrough(); export const cassetteSchema = z.object({ - version: z.literal(CURRENT_FORMAT_VERSION), + version: z.any(), // TODO(luca): Remove after cassettes no longer have this field meta: cassetteMetaSchema.optional(), entries: z.array(cassetteEntrySchema), }); diff --git a/dev-packages/seinfeld/src/store/file-store.ts b/dev-packages/seinfeld/src/store/file-store.ts index 900071c42..4bc5d6d6a 100644 --- a/dev-packages/seinfeld/src/store/file-store.ts +++ b/dev-packages/seinfeld/src/store/file-store.ts @@ -37,8 +37,6 @@ export interface JsonFileStoreOptions { * directory, e.g. `outer.cassette.blobs/.bin`. * * - `load` returns `null` when the file doesn't exist. - * - `load` throws `CassetteVersionError` when the file's version is newer than - * the library supports, and `CassetteFormatError` on schema mismatches. * - `save` writes atomically via a temp file + rename. If two workers race on * the same cassette, the last writer wins; no half-written files are left. */ From 203906c182a96952eb2b7853629028df7de97fb0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 17:35:56 +0200 Subject: [PATCH 6/8] update cl message --- .changeset/tangy-waves-find.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tangy-waves-find.md b/.changeset/tangy-waves-find.md index 629d1cca4..64617cdf4 100644 --- a/.changeset/tangy-waves-find.md +++ b/.changeset/tangy-waves-find.md @@ -2,4 +2,4 @@ "braintrust": minor --- -feat(bundler-plugins): Add `braintrust{Bundler}Plugin` aliases for bundler plugins and deprecate rest +feat(bundler-plugins): Add `braintrustVitePlugin`, `braintrustWebpackPlugin`, `braintrustEsbuildPlugin`, `braintrustRollupPlugin` aliases for bundler plugins and deprecate old ones From 6a5d12c836603b6d4472f2c9d7c3aa4ec47b3859 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 17:39:26 +0200 Subject: [PATCH 7/8] rm unnecessary note --- js/src/auto-instrumentations/bundler/esbuild.ts | 5 +---- js/src/auto-instrumentations/bundler/rollup.ts | 5 +---- js/src/auto-instrumentations/bundler/vite.ts | 5 +---- js/src/auto-instrumentations/bundler/webpack.ts | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/js/src/auto-instrumentations/bundler/esbuild.ts b/js/src/auto-instrumentations/bundler/esbuild.ts index 8c2576302..85c0d0375 100644 --- a/js/src/auto-instrumentations/bundler/esbuild.ts +++ b/js/src/auto-instrumentations/bundler/esbuild.ts @@ -18,9 +18,6 @@ export function braintrustEsbuildPlugin( export type EsbuildPluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustEsbuildPlugin} instead. This legacy export - * defaults to browser-compatible diagnostics channel shimming when `browser` - * is omitted; `braintrustEsbuildPlugin` defaults to Node.js diagnostics_channel - * unless `useDiagnosticChannelCompatShim` is set to `true`. + * @deprecated Use {@link braintrustEsbuildPlugin} instead. */ export const esbuildPlugin = unplugin.esbuild; diff --git a/js/src/auto-instrumentations/bundler/rollup.ts b/js/src/auto-instrumentations/bundler/rollup.ts index 3f9d2a9a1..981bff88c 100644 --- a/js/src/auto-instrumentations/bundler/rollup.ts +++ b/js/src/auto-instrumentations/bundler/rollup.ts @@ -18,9 +18,6 @@ export function braintrustRollupPlugin( export type RollupPluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustRollupPlugin} instead. This legacy export - * defaults to browser-compatible diagnostics channel shimming when `browser` - * is omitted; `braintrustRollupPlugin` defaults to Node.js diagnostics_channel - * unless `useDiagnosticChannelCompatShim` is set to `true`. + * @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 004cc53ea..427e16bee 100644 --- a/js/src/auto-instrumentations/bundler/vite.ts +++ b/js/src/auto-instrumentations/bundler/vite.ts @@ -18,9 +18,6 @@ export function braintrustVitePlugin( export type VitePluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustVitePlugin} instead. This legacy export - * defaults to browser-compatible diagnostics channel shimming when `browser` - * is omitted; `braintrustVitePlugin` defaults to Node.js diagnostics_channel - * unless `useDiagnosticChannelCompatShim` is set to `true`. + * @deprecated Use {@link braintrustVitePlugin} instead. */ export const vitePlugin = unplugin.vite; diff --git a/js/src/auto-instrumentations/bundler/webpack.ts b/js/src/auto-instrumentations/bundler/webpack.ts index 23c1e237d..8c3ed1c7d 100644 --- a/js/src/auto-instrumentations/bundler/webpack.ts +++ b/js/src/auto-instrumentations/bundler/webpack.ts @@ -18,9 +18,6 @@ export function braintrustWebpackPlugin( export type WebpackPluginOptions = LegacyBundlerPluginOptions; /** - * @deprecated Use {@link braintrustWebpackPlugin} instead. This legacy export - * defaults to browser-compatible diagnostics channel shimming when `browser` - * is omitted; `braintrustWebpackPlugin` defaults to Node.js diagnostics_channel - * unless `useDiagnosticChannelCompatShim` is set to `true`. + * @deprecated Use {@link braintrustWebpackPlugin} instead. */ export const webpackPlugin = unplugin.webpack; From c88abdfc088dd7139439635af7d97ef668393ae9 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 May 2026 17:40:02 +0200 Subject: [PATCH 8/8] fix build --- dev-packages/seinfeld/src/recorder.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev-packages/seinfeld/src/recorder.ts b/dev-packages/seinfeld/src/recorder.ts index 24665355b..afa04e36b 100644 --- a/dev-packages/seinfeld/src/recorder.ts +++ b/dev-packages/seinfeld/src/recorder.ts @@ -17,7 +17,6 @@ import type { RecordedRequest, } from "./cassette"; import { AggregateCassetteMissError, CassetteMissError } from "./errors"; -import { CURRENT_FORMAT_VERSION } from "./format"; import { applyFilters } from "./normalizer"; import type { FilterSpec } from "./normalizer"; import { asNormalized } from "./matcher"; @@ -362,7 +361,6 @@ async function persistIfRecord(ctx: CassetteContext): Promise { // Ignore load errors (corrupt file, version mismatch) — stamp fresh. } const cassette: CassetteFile = { - version: CURRENT_FORMAT_VERSION, meta: { createdAt }, entries: flushedEntries, };