Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tangy-waves-find.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": minor
---

feat(bundler-plugins): Add `braintrustVitePlugin`, `braintrustWebpackPlugin`, `braintrustEsbuildPlugin`, `braintrustRollupPlugin` aliases for bundler plugins and deprecate old ones
1 change: 0 additions & 1 deletion dev-packages/seinfeld/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "@braintrust/seinfeld",
"version": "0.0.0",
"private": true,
"description": "Internal cassette server for e2e provider tests.",
"type": "module",
Expand Down
3 changes: 0 additions & 3 deletions dev-packages/seinfeld/src/cassette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also gotta update dev-packages/seinfeld/src/format/v1.ts

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, the rabbit hole went even deeper cad5d4a

}

/**
Expand All @@ -87,7 +85,6 @@ export interface CassetteMeta {
* incompatibly.
*/
export interface CassetteFile {
version: 1;
meta?: CassetteMeta;
entries: CassetteEntry[];
}
Expand Down
27 changes: 0 additions & 27 deletions dev-packages/seinfeld/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
65 changes: 7 additions & 58 deletions dev-packages/seinfeld/src/format/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 2 additions & 14 deletions dev-packages/seinfeld/src/format/v1.ts
Original file line number Diff line number Diff line change
@@ -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() }),
Expand Down Expand Up @@ -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),
});
10 changes: 1 addition & 9 deletions dev-packages/seinfeld/src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -368,8 +361,7 @@ async function persistIfRecord(ctx: CassetteContext): Promise<void> {
// 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);
Expand Down
2 changes: 0 additions & 2 deletions dev-packages/seinfeld/src/store/file-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export interface JsonFileStoreOptions {
* directory, e.g. `outer.cassette.blobs/<sha256>.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.
*/
Expand Down
6 changes: 0 additions & 6 deletions dev-packages/seinfeld/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineConfig } from "tsup";
import pkg from "./package.json";

export default defineConfig({
entry: {
Expand All @@ -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),
},
});
1 change: 0 additions & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "@braintrust/js-e2e-tests",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
16 changes: 8 additions & 8 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
};
```

Expand Down
37 changes: 15 additions & 22 deletions js/src/auto-instrumentations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,39 +245,36 @@ 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

**For browser/edge builds:**

```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

Expand Down Expand Up @@ -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:**

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading