diff --git a/.changeset/tangy-waves-find.md b/.changeset/tangy-waves-find.md new file mode 100644 index 000000000..64617cdf4 --- /dev/null +++ b/.changeset/tangy-waves-find.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat(bundler-plugins): Add `braintrustVitePlugin`, `braintrustWebpackPlugin`, `braintrustEsbuildPlugin`, `braintrustRollupPlugin` aliases for bundler plugins and deprecate old ones 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..2ca69b387 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; } /** @@ -87,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/recorder.ts b/dev-packages/seinfeld/src/recorder.ts index dbd0bbf83..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"; @@ -39,12 +38,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; @@ -368,8 +361,7 @@ async function persistIfRecord(ctx: CassetteContext): Promise { // Ignore load errors (corrupt file, version mismatch) — stamp fresh. } 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/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. */ 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/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 98b69c62d..5ce49ff84 100644 --- a/js/src/auto-instrumentations/README.md +++ b/js/src/auto-instrumentations/README.md @@ -245,16 +245,14 @@ 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 - ], + 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 @@ -262,22 +260,21 @@ 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 - ], + 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 ### Next.js Setup @@ -382,7 +379,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:** @@ -391,8 +388,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) @@ -460,14 +457,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..85c0d0375 100644 --- a/js/src/auto-instrumentations/bundler/esbuild.ts +++ b/js/src/auto-instrumentations/bundler/esbuild.ts @@ -1,24 +1,23 @@ -/** - * esbuild plugin for auto-instrumentation. - * - * Usage: - * ```typescript - * import { esbuildPlugin } from '@braintrust/auto-instrumentations/bundler/esbuild'; - * - * await esbuild.build({ - * plugins: [esbuildPlugin()], - * }); - * ``` - * - * 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 type { EsbuildPlugin } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -import { unplugin, type BundlerPluginOptions } from "./plugin"; +export function braintrustEsbuildPlugin( + options: BundlerPluginOptions = {}, +): EsbuildPlugin { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.esbuild({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export type EsbuildPluginOptions = BundlerPluginOptions; +export type EsbuildPluginOptions = LegacyBundlerPluginOptions; +/** + * @deprecated Use {@link braintrustEsbuildPlugin} instead. + */ export const esbuildPlugin = unplugin.esbuild; diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index ba215101d..d374499b5 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, @@ -22,7 +9,7 @@ import { fileURLToPath } from "url"; import moduleDetailsFromPath from "module-details-from-path"; import { getDefaultInstrumentationConfigs } from "../configs/all"; -export interface BundlerPluginOptions { +export interface LegacyBundlerPluginOptions { /** * Enable debug logging */ @@ -43,6 +30,31 @@ export interface BundlerPluginOptions { browser?: boolean; } +export interface BundlerPluginOptions { + /** + * Enable debug logging + */ + debug?: boolean; + + /** + * Additional instrumentation configs to apply + */ + instrumentations?: InstrumentationConfig[]; + + /** + * 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 */ @@ -60,96 +72,98 @@ function getModuleVersion(basedir: string): string | undefined { return undefined; // No version found } -export const unplugin = createUnplugin((options = {}) => { - const allInstrumentations = getDefaultInstrumentationConfigs({ - additionalInstrumentations: 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 = getDefaultInstrumentationConfigs({ + additionalInstrumentations: 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 c89fbff3f..981bff88c 100644 --- a/js/src/auto-instrumentations/bundler/rollup.ts +++ b/js/src/auto-instrumentations/bundler/rollup.ts @@ -1,24 +1,23 @@ -/** - * Rollup plugin for auto-instrumentation. - * - * Usage: - * ```typescript - * import { rollupPlugin } from '@braintrust/auto-instrumentations/bundler/rollup'; - * - * export default { - * plugins: [rollupPlugin()] - * }; - * ``` - * - * 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 type { RollupPlugin } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -import { unplugin, type BundlerPluginOptions } from "./plugin"; +export function braintrustRollupPlugin( + options: BundlerPluginOptions = {}, +): RollupPlugin | RollupPlugin[] { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.rollup({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export type RollupPluginOptions = BundlerPluginOptions; +export type RollupPluginOptions = LegacyBundlerPluginOptions; +/** + * @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 ceb220f9a..427e16bee 100644 --- a/js/src/auto-instrumentations/bundler/vite.ts +++ b/js/src/auto-instrumentations/bundler/vite.ts @@ -1,24 +1,23 @@ -/** - * Vite plugin for auto-instrumentation. - * - * Usage: - * ```typescript - * import { vitePlugin } from '@braintrust/auto-instrumentations/bundler/vite'; - * - * export default { - * plugins: [vitePlugin()], - * }; - * ``` - * - * 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 type { VitePlugin } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -import { unplugin, type BundlerPluginOptions } from "./plugin"; +export function braintrustVitePlugin( + options: BundlerPluginOptions = {}, +): VitePlugin | VitePlugin[] { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.vite({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export type VitePluginOptions = BundlerPluginOptions; +export type VitePluginOptions = LegacyBundlerPluginOptions; +/** + * @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 f7c5d1a6f..1f18fcddb 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -30,7 +30,7 @@ import { extname, join, sep } from "path"; import { readFileSync } from "fs"; import moduleDetailsFromPath from "module-details-from-path"; import { getDefaultInstrumentationConfigs } from "../configs/all"; -import { type BundlerPluginOptions } from "./plugin"; +import { type LegacyBundlerPluginOptions } from "./plugin"; /** * Helper function to get module version from package.json @@ -55,7 +55,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 = getDefaultInstrumentationConfigs({ additionalInstrumentations: options.instrumentations, }); @@ -90,7 +92,7 @@ process.on("exit", () => { /** * Webpack loader that instruments JavaScript code using code-transformer. * - * Accepts the same options as the webpack plugin (BundlerPluginOptions). + * Accepts the same options as the legacy webpack plugin. */ function codeTransformerLoader( this: any, @@ -98,7 +100,7 @@ function codeTransformerLoader( inputSourceMap?: any, ): void { const callback = this.async(); - const options: BundlerPluginOptions = this.getOptions() ?? {}; + const options: LegacyBundlerPluginOptions = this.getOptions() ?? {}; const resourcePath: string = this.resourcePath; // Skip virtual modules (e.g. Next.js loaders pass query-string URLs with no real path) @@ -163,7 +165,7 @@ function codeTransformerLoader( // Attach Options type to the loader function namespace codeTransformerLoader { - export type Options = BundlerPluginOptions; + export type Options = LegacyBundlerPluginOptions; } export = codeTransformerLoader; diff --git a/js/src/auto-instrumentations/bundler/webpack.ts b/js/src/auto-instrumentations/bundler/webpack.ts index 19497e77c..8c3ed1c7d 100644 --- a/js/src/auto-instrumentations/bundler/webpack.ts +++ b/js/src/auto-instrumentations/bundler/webpack.ts @@ -1,24 +1,23 @@ -/** - * Webpack plugin for auto-instrumentation. - * - * Usage: - * ```javascript - * import { webpackPlugin } from 'braintrust/auto-instrumentations/bundler/webpack'; - * - * export default { - * plugins: [webpackPlugin()], - * }; - * ``` - * - * 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 type { WebpackPluginInstance } from "unplugin"; +import { + BundlerPluginOptions, + unplugin, + type LegacyBundlerPluginOptions, +} from "./plugin"; -import { unplugin, type BundlerPluginOptions } from "./plugin"; +export function braintrustWebpackPlugin( + options: BundlerPluginOptions = {}, +): WebpackPluginInstance { + const { useDiagnosticChannelCompatShim = false, ...pluginOptions } = options; + return unplugin.webpack({ + ...pluginOptions, + browser: useDiagnosticChannelCompatShim, + }); +} -export type WebpackPluginOptions = BundlerPluginOptions; +export type WebpackPluginOptions = LegacyBundlerPluginOptions; +/** + * @deprecated Use {@link braintrustWebpackPlugin} instead. + */ export const webpackPlugin = unplugin.webpack; 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/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": { diff --git a/js/tests/auto-instrumentations/transformation.test.ts b/js/tests/auto-instrumentations/transformation.test.ts index 44ce291ca..fdc3bc4c6 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); @@ -369,7 +370,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"); @@ -391,10 +392,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 }); @@ -413,11 +411,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"); @@ -450,7 +449,7 @@ describe("Orchestrion Transformation Tests", () => { input: entryPoint, plugins: [ resolverPlugin, - rollupPlugin({ browser: true }), // Use browser mode + braintrustRollupPlugin({ useDiagnosticChannelCompatShim: true }), ], external: [], preserveSymlinks: true,