Skip to content
Open
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/honest-buckets-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": minor
---

feat: Add `braintrust/apply-instrumentation` entrypoint for CJS/TS patching
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.

should we call it braintrust/apply-auto-instrumentation instead?

14 changes: 14 additions & 0 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": true,
"browser": {
"./dist/index.js": "./dist/browser.js",
"./dist/index.d.ts": "./dist/browser.d.ts",
Expand Down Expand Up @@ -50,6 +51,19 @@
"require": "./dist/browser.js",
"default": "./dist/browser.mjs"
},
"./apply-instrumentation": {
"types": "./dist/apply-instrumentation.d.ts",
"edge-light": "./dist/apply-instrumentation.browser.mjs",
"workerd": "./dist/apply-instrumentation.browser.mjs",
"node": {
"import": "./dist/apply-instrumentation.mjs",
"require": "./dist/apply-instrumentation.js"
},
"browser": "./dist/apply-instrumentation.browser.mjs",
"import": "./dist/apply-instrumentation.mjs",
"require": "./dist/apply-instrumentation.js",
"default": "./dist/apply-instrumentation.mjs"
},
"./node": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
Expand Down
64 changes: 43 additions & 21 deletions js/src/auto-instrumentations/configs/all.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { InstrumentationConfig } from "@apm-js-collab/code-transformer";
import {
isInstrumentationIntegrationDisabled,
readDisabledInstrumentationEnvConfig,
type InstrumentationIntegrationsConfig,
} from "../../instrumentation/config";
import { aiSDKConfigs } from "./ai-sdk";
import { anthropicConfigs } from "./anthropic";
import { claudeAgentSDKConfigs } from "./claude-agent-sdk";
Expand All @@ -18,70 +23,87 @@ import { openRouterConfigs } from "./openrouter";
import { openRouterAgentConfigs } from "./openrouter-agent";

interface InstrumentationConfigGroup {
disabledNames: readonly string[];
integrations: readonly (keyof InstrumentationIntegrationsConfig)[];
configs: readonly InstrumentationConfig[];
}

const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] =
[
{ disabledNames: ["openai"], configs: openaiConfigs },
{ integrations: ["openai"], configs: openaiConfigs },
{
disabledNames: ["openai-codex", "openai-codex-sdk", "codex", "codex-sdk"],
integrations: ["openaiCodexSDK"],
configs: openAICodexConfigs,
},
{ disabledNames: ["anthropic"], configs: anthropicConfigs },
{ integrations: ["anthropic"], configs: anthropicConfigs },
{
disabledNames: ["aisdk", "ai-sdk", "vercel-ai"],
integrations: ["aisdk", "vercel"],
configs: aiSDKConfigs,
},
{
disabledNames: ["claudeagentsdk", "claude-agent-sdk"],
integrations: ["claudeAgentSDK"],
configs: claudeAgentSDKConfigs,
},
{ disabledNames: ["cursor", "cursor-sdk"], configs: cursorSDKConfigs },
{ integrations: ["cursor", "cursorSDK"], configs: cursorSDKConfigs },
{
disabledNames: ["openai-agents", "openaiagents", "openai-agents-core"],
integrations: ["openAIAgents"],
configs: openAIAgentsCoreConfigs,
},
{
disabledNames: ["google", "google-genai"],
integrations: ["google", "googleGenAI"],
configs: googleGenAIConfigs,
},
{ disabledNames: ["huggingface"], configs: huggingFaceConfigs },
{ disabledNames: ["openrouter"], configs: openRouterConfigs },
{ integrations: ["huggingface"], configs: huggingFaceConfigs },
{ integrations: ["openrouter"], configs: openRouterConfigs },
{
disabledNames: ["openrouteragent", "openrouter-agent"],
integrations: ["openrouterAgent"],
configs: openRouterAgentConfigs,
},
{ disabledNames: ["mistral"], configs: mistralConfigs },
{ disabledNames: ["googleadk", "google-adk"], configs: googleADKConfigs },
{ disabledNames: ["cohere"], configs: cohereConfigs },
{ disabledNames: ["groq", "groq-sdk"], configs: groqConfigs },
{ integrations: ["mistral"], configs: mistralConfigs },
{ integrations: ["googleADK"], configs: googleADKConfigs },
{ integrations: ["cohere"], configs: cohereConfigs },
{ integrations: ["groq"], configs: groqConfigs },
{
disabledNames: ["genkit", "firebase-genkit"],
integrations: ["genkit"],
configs: genkitConfigs,
},
{
disabledNames: ["githubcopilot", "github-copilot", "copilot-sdk"],
integrations: ["gitHubCopilot"],
configs: gitHubCopilotConfigs,
},
];

export function getDefaultInstrumentationConfigs({
additionalInstrumentations,
disabledIntegrationConfig,
disabledIntegrations,
}: {
additionalInstrumentations?: readonly InstrumentationConfig[];
disabledIntegrationConfig?: InstrumentationIntegrationsConfig;
disabledIntegrations?: ReadonlySet<string>;
} = {}): InstrumentationConfig[] {
const disabledConfig =
disabledIntegrationConfig ??
(disabledIntegrations
? readDisabledInstrumentationEnvConfig(
[...disabledIntegrations].join(","),
).integrations
: undefined);

return [
...defaultInstrumentationConfigGroups.flatMap(
({ configs, disabledNames }) =>
disabledIntegrations &&
disabledNames.some((name) => disabledIntegrations.has(name))
({ configs, integrations }) =>
isInstrumentationIntegrationDisabled(disabledConfig, ...integrations)
? []
: configs,
),
...(additionalInstrumentations ?? []),
];
}

export function getDefaultAutoInstrumentationConfigs(): InstrumentationConfig[] {
return getDefaultInstrumentationConfigs({
disabledIntegrationConfig: readDisabledInstrumentationEnvConfig(
process.env.BRAINTRUST_DISABLE_INSTRUMENTATION,
).integrations,
});
}
84 changes: 38 additions & 46 deletions js/src/auto-instrumentations/hook.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,62 +14,54 @@
*/

import { register } from "node:module";
import { getDefaultInstrumentationConfigs } from "./configs/all.js";
import { getDefaultAutoInstrumentationConfigs } from "./configs/all.js";
import { ModulePatch } from "./loader/cjs-patch.js";
import { patchTracingChannel } from "./patch-tracing-channel.js";

const state = ((globalThis as any)[
Symbol.for("braintrust.applyInstrumentation")
] ??= {}) as { applied?: boolean };
const alreadyApplied = state.applied;

// Patch diagnostics_channel.tracePromise to handle APIPromise correctly.
// MUST be done here (before any SDK code runs) to fix Anthropic APIPromise incompatibility.
// Construct the module path dynamically to prevent build from stripping "node:" prefix.
const dcPath = ["node", "diagnostics_channel"].join(":");
const dc: any = await import(/* @vite-ignore */ dcPath as any);
patchTracingChannel(dc.tracingChannel);

function readDisabledIntegrations(): Set<string> {
const raw = process.env.BRAINTRUST_DISABLE_INSTRUMENTATION;
if (!raw) {
return new Set();
}

return new Set(
raw
.split(",")
.map((value) => value.trim().toLowerCase())
.filter((value) => value.length > 0),
);
if (!alreadyApplied) {
const dcPath = ["node", "diagnostics_channel"].join(":");
const dc: any = await import(/* @vite-ignore */ dcPath as any);
patchTracingChannel(dc.tracingChannel);
}

// Combine all instrumentation configs.
// Respect BRAINTRUST_DISABLE_INSTRUMENTATION here too so load-time
// transformation and runtime plugins stay aligned.
const allConfigs = getDefaultInstrumentationConfigs({
disabledIntegrations: readDisabledIntegrations(),
});
if (!alreadyApplied) {
const allConfigs = getDefaultAutoInstrumentationConfigs();

// 1. Register ESM loader for ESM modules
register("./loader/esm-hook.mjs", {
parentURL: import.meta.url,
data: { instrumentations: allConfigs },
} as any);
// 1. Register ESM loader for ESM modules
register("./loader/esm-hook.mjs", {
parentURL: import.meta.url,
data: { instrumentations: allConfigs },
} as any);

// 2. Also load CJS register for CJS modules (many apps use mixed ESM/CJS)
try {
const patch = new ModulePatch({ instrumentations: allConfigs });
patch.patch();
state.applied = true;

if (process.env.DEBUG === "@braintrust*" || process.env.DEBUG === "*") {
console.log(
"[Braintrust] Auto-instrumentation active (ESM + CJS) for:",
allConfigs.map((c) => c.channelName).join(", "),
);
}
} catch (err) {
// CJS patch failed, but ESM hook is still active
if (process.env.DEBUG === "@braintrust*" || process.env.DEBUG === "*") {
console.log(
"[Braintrust] Auto-instrumentation active (ESM only) for:",
allConfigs.map((c) => c.channelName).join(", "),
);
console.error("[Braintrust] CJS patch failed:", err);
// 2. Also load CJS register for CJS modules (many apps use mixed ESM/CJS)
try {
const patch = new ModulePatch({ instrumentations: allConfigs });
patch.patch();

if (process.env.DEBUG === "@braintrust*" || process.env.DEBUG === "*") {
console.log(
"[Braintrust] Auto-instrumentation active (ESM + CJS) for:",
allConfigs.map((c) => c.channelName).join(", "),
);
}
} catch (err) {
// CJS patch failed, but ESM hook is still active
if (process.env.DEBUG === "@braintrust*" || process.env.DEBUG === "*") {
console.log(
"[Braintrust] Auto-instrumentation active (ESM only) for:",
allConfigs.map((c) => c.channelName).join(", "),
);
console.error("[Braintrust] CJS patch failed:", err);
}
}
}
24 changes: 2 additions & 22 deletions js/src/instrumentation/braintrust-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,10 @@ import { CoherePlugin } from "./plugins/cohere-plugin";
import { GroqPlugin } from "./plugins/groq-plugin";
import { GenkitPlugin } from "./plugins/genkit-plugin";
import { GitHubCopilotPlugin } from "./plugins/github-copilot-plugin";
import type { InstrumentationIntegrationsConfig } from "./config";

export interface BraintrustPluginConfig {
integrations?: {
openai?: boolean;
anthropic?: boolean;
vercel?: boolean;
aisdk?: boolean;
google?: boolean;
googleGenAI?: boolean;
huggingface?: boolean;
claudeAgentSDK?: boolean;
cursor?: boolean;
cursorSDK?: boolean;
openrouter?: boolean;
openrouterAgent?: boolean;
mistral?: boolean;
googleADK?: boolean;
cohere?: boolean;
groq?: boolean;
genkit?: boolean;
gitHubCopilot?: boolean;
openaiCodexSDK?: boolean;
openAIAgents?: boolean;
};
integrations?: InstrumentationIntegrationsConfig;
}

function getIntegrationConfig(
Expand Down
Loading
Loading