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/chilly-ends-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": patch
---

fix(instrumentation): Initialize tracing automatically in bundles
4 changes: 0 additions & 4 deletions e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
// Bundler transforms and runtime consumers are enabled independently.
import * as cloudflareThink from "@cloudflare/think";
import { createOpenAI } from "@ai-sdk/openai";
import { flush, initLogger, wrapCloudflareThink } from "braintrust";
import { tool } from "ai";
import { z } from "zod";

if (process.env.CLOUDFLARE_THINK_INSTRUMENTATION !== "manual")
await import("braintrust/auto");

const OPENAI_MODEL = "gpt-5-nano";

type Env = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import "braintrust/auto"; // Registers Braintrust's global instrumentation hooks.
import OpenAI from "openai";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
Expand Down
14 changes: 12 additions & 2 deletions js/src/auto-instrumentations/bundler/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createUnplugin, type UnpluginInstance } from "unplugin";
import { create, type InstrumentationConfig } from "../orchestrion-js";
import { Transformer } from "../orchestrion-js/transformer";
import { extname, isAbsolute, join, sep } from "path";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
Expand Down Expand Up @@ -129,7 +130,12 @@ export const unplugin: UnpluginInstance<BundlerPluginOptions, boolean> =
browser,
});
if (patched !== null) {
return { code: patched, map: null };
return new Transformer(
moduleName,
moduleVersion ?? "",
normalizedModulePath,
[],
).transform(patched, isModule ? "esm" : "cjs", "braintrust/auto");
}

// If no version found
Expand All @@ -155,7 +161,11 @@ export const unplugin: UnpluginInstance<BundlerPluginOptions, boolean> =
try {
// Transform the code
const moduleType = isModule ? "esm" : "cjs";
const result = transformer.transform(code, moduleType);
const result = transformer.transform(
code,
moduleType,
"braintrust/auto",
);
return {
code: result.code,
map: result.map,
Expand Down
11 changes: 9 additions & 2 deletions js/src/auto-instrumentations/bundler/webpack-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { create } from "../orchestrion-js";
import { Transformer } from "../orchestrion-js/transformer";
import { extname, join, sep } from "path";
import { readFileSync } from "fs";
import moduleDetailsFromPath from "module-details-from-path";
Expand Down Expand Up @@ -120,7 +121,13 @@ function codeTransformerLoader(
browser: options.browser ?? false,
});
if (patched !== null) {
return callback(null, patched);
const result = new Transformer(
moduleName,
"",
normalizedModulePath,
[],
).transform(patched, isModule ? "esm" : "cjs", "braintrust/auto");
return callback(null, result.code, result.map);
}

const moduleVersion = getModuleVersion(moduleDetails.basedir);
Expand All @@ -141,7 +148,7 @@ function codeTransformerLoader(

try {
const moduleType: ModuleType = isModule ? "esm" : "cjs";
const result = transformer.transform(code, moduleType);
const result = transformer.transform(code, moduleType, "braintrust/auto");
callback(null, result.code, result.map ?? undefined);
} catch (error) {
console.warn(
Expand Down
31 changes: 30 additions & 1 deletion js/src/auto-instrumentations/orchestrion-js/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export class Transformer {
* Instruments `code` by injecting global tracing hooks around the
* target functions defined by this transformer's configs.
*/
transform(code: string | Buffer, moduleType: ModuleType): TransformOutput {
transform(
code: string | Buffer,
moduleType: ModuleType,
runtimeImport?: string,
): TransformOutput {
if (Buffer.isBuffer(code)) {
code = code.toString();
}
Expand Down Expand Up @@ -126,6 +130,31 @@ export class Transformer {
);
}

if (runtimeImport) {
// Bundlers need the consumers in the application, whereas Node hooks
// register them in their preload. Insert into the AST to preserve both
// directive prologues (especially CJS strict mode) and source mappings.
ast ??= parse(code, {
loc: true,
ranges: true,
raw: true,
module: moduleType === "esm",
});
const statement =
moduleType === "esm"
? `import ${JSON.stringify(runtimeImport)};`
: `require(${JSON.stringify(runtimeImport)});`;
const initialization = parse(statement, { module: moduleType === "esm" });
let index = 0;
while (
ast.body[index]?.type === "ExpressionStatement" &&
typeof ast.body[index].expression.value === "string"
) {
index++;
}
ast.body.splice(index, 0, ...initialization.body);
}

if (ast) {
const file = `${this.moduleName}/${this.filePath}`;
const sourceMap = new SourceMapGenerator({ file });
Expand Down
18 changes: 8 additions & 10 deletions js/src/imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,17 @@ describe("Import restrictions", () => {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n");

// Exception: the Mastra observability patch *generates* CJS source code
// (a string of JavaScript that is appended to @mastra/core's
// dist/mastra/index.cjs). That source legitimately contains
// `require(...)` because it runs inside a CommonJS module's evaluation
// — it isn't a require() call from our SDK code itself. Normalize the
// separator so the exemption matches on Windows (`\\`) too.
const isMastraPatchGenerator = relativePath
.replaceAll(path.sep, "/")
.includes("auto-instrumentations/loader/mastra-observability-patch.ts");
// These generators emit CJS source containing require() calls for the
// instrumented module to execute, rather than calling require() in the SDK.
// Normalize separators so the exceptions also match on Windows.
const isCommonJsGenerator = [
"auto-instrumentations/loader/mastra-observability-patch.ts",
"auto-instrumentations/orchestrion-js/transformer.ts",
].includes(relativePath.replaceAll(path.sep, "/"));

lines.forEach((line, index) => {
// Check for require() calls
if (/\brequire\s*\(/.test(line) && !isMastraPatchGenerator) {
if (/\brequire\s*\(/.test(line) && !isCommonJsGenerator) {
violations.push(
`${relativePath}:${index + 1} - Found require() statement: "${line.trim()}"`,
);
Expand Down
5 changes: 5 additions & 0 deletions js/tests/auto-instrumentations/test-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type Bundler = "esbuild" | "vite" | "rollup";
* Bundles a fixture entry (which imports packages from fixtures/node_modules)
* with the Braintrust plugin for the given bundler and writes an ESM bundle to
* `outfile`.
* Keep the runtime external so these tests isolate the generated hooks; e2e
* scenarios verify bundled consumer initialization and span creation.
*/
export async function bundleFixture(
bundler: Bundler,
Expand All @@ -34,6 +36,7 @@ export async function bundleFixture(
absWorkingDir: fixturesDir,
preserveSymlinks: true, // Don't dereference symlinks
platform: "node",
external: ["braintrust/auto"],
});
} else if (bundler === "vite") {
const { braintrustVitePlugin } =
Expand All @@ -49,6 +52,7 @@ export async function bundleFixture(
outDir: path.dirname(outfile),
emptyOutDir: false,
minify: false,
rolldownOptions: { external: ["braintrust/auto"] },
},
plugins: [braintrustVitePlugin(options)],
logLevel: "error",
Expand All @@ -59,6 +63,7 @@ export async function bundleFixture(
await import("../../src/auto-instrumentations/bundler/rollup.js");
const bundle = await rollup({
input: entryPoint,
external: ["braintrust/auto"],
plugins: [
{
name: "resolve-fixture-packages",
Expand Down
41 changes: 41 additions & 0 deletions js/tests/auto-instrumentations/transformation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { runInNewContext } from "node:vm";
import { SourceMapConsumer } from "source-map";
import { Transformer } from "../../src/auto-instrumentations/orchestrion-js/transformer";
import {
create,
type InstrumentationConfig,
Expand Down Expand Up @@ -81,6 +83,7 @@ function transformTestCode(
}

function expectGlobalHookTransform(output: string): void {
expect(output).toContain("braintrust/auto");
expect(output).toContain("__braintrust_instrumentation_hooks");
expect(output).toContain("braintrust.global-instrumentation-hooks.registry");
expect(output).toContain("braintrust.global-instrumentation-hooks.hook");
Expand All @@ -97,6 +100,37 @@ function expectGlobalHookTransform(output: string): void {
}

describe("Orchestrion Transformation Tests", () => {
it("initializes consumers before CommonJS calls without changing strict mode or source maps", async () => {
const source =
'"use strict";\nfunction request() { return this; }\nmodule.exports = request();';
const transformer = new Transformer("test-sdk", "1.0.0", "index.cjs", [
testConfig({ functionName: "request", kind: "Sync" }),
]);
const result = transformer.transform(source, "cjs", "braintrust/auto");
const loadedModule = { exports: "not called" };
const require = vi.fn((id: string) => {
expect(id).toBe("braintrust/auto");
expect(loadedModule.exports).toBe("not called");
});

Function("require", "module", result.code)(require, loadedModule);
expect(require).toHaveBeenCalledOnce();
expect(loadedModule.exports).toBeUndefined();

const lines = result.code.split("\n");
const generatedLine =
lines.findIndex((line) => line.startsWith("module.exports")) + 1;
await SourceMapConsumer.with(result.map!, null, (consumer) => {
expect(
consumer.originalPositionFor({ line: generatedLine, column: 0 }),
).toMatchObject({
source: "test-sdk/index.cjs",
line: 3,
column: 0,
});
});
});

beforeAll(() => {
// Create output directory
if (!fs.existsSync(outputDir)) {
Expand Down Expand Up @@ -705,6 +739,7 @@ describe("Orchestrion Transformation Tests", () => {
library: { type: "module" },
},
experiments: { outputModule: true },
externals: { "braintrust/auto": "module braintrust/auto" },
mode: "development",
resolve: { modules: [nodeModulesDir, "node_modules"] },
plugins: [braintrustWebpackPlugin({ browser })],
Expand Down Expand Up @@ -758,6 +793,7 @@ describe("Orchestrion Transformation Tests", () => {
library: { type: "module" },
},
experiments: { outputModule: true },
externals: { "braintrust/auto": "module braintrust/auto" },
mode: "development",
resolve: { modules: [nodeModulesDir, "node_modules"] },
// No plugins — only the loader, mirroring turbopack's constraint
Expand Down Expand Up @@ -826,6 +862,7 @@ describe("Orchestrion Transformation Tests", () => {
experiments: { outputModule: true },
mode: "development",
target,
externals: { "braintrust/auto": "module braintrust/auto" },
module: {
rules: [
{
Expand All @@ -842,9 +879,11 @@ describe("Orchestrion Transformation Tests", () => {

expect(errors).toHaveLength(0);
if (shouldPatch) {
expect(output).toContain("braintrust/auto");
expect(output).toContain("__braintrustObservabilityClass");
expect(output).toContain("@mastra/observability");
} else {
expect(output).not.toContain("braintrust/auto");
expect(output).not.toContain("__braintrustObservabilityClass");
expect(output).not.toContain("node:module");
}
Expand All @@ -862,6 +901,7 @@ describe("Orchestrion Transformation Tests", () => {
experiments: { outputModule: true },
mode: "development",
target: "web",
externals: { "braintrust/auto": "module braintrust/auto" },
module: {
rules: [
{
Expand All @@ -878,6 +918,7 @@ describe("Orchestrion Transformation Tests", () => {

expect(errors).toHaveLength(0);
expect(output).toContain("__btPatchAPIPromise");
expect(output).toContain("braintrust/auto");
expect(output).toContain("__btParsePatched");
});
});
Expand Down
Loading