diff --git a/js/dev/authorize.ts b/js/dev/authorize.ts index 4f7592d17..eb91a2aaf 100644 --- a/js/dev/authorize.ts +++ b/js/dev/authorize.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../src/debug-logger"; import { Request, Response, NextFunction } from "express"; import { IncomingHttpHeaders } from "http"; import createError from "http-errors"; @@ -90,7 +91,7 @@ export function makeCheckAuthorized(allowedOrgName: string | undefined) { req.ctx.state = state; next(); } catch (e) { - console.error("Authorization error:", e); + debugLogger.notice.error("Authorization error:", e); return next(createError(401, "Unauthorized")); } }; diff --git a/js/dev/errorHandler.ts b/js/dev/errorHandler.ts index 12cd15e24..04f8d8fc6 100644 --- a/js/dev/errorHandler.ts +++ b/js/dev/errorHandler.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../src/debug-logger"; import { z } from "zod/v3"; import { Request, Response, ErrorRequestHandler, NextFunction } from "express"; import { HttpError } from "http-errors"; @@ -27,7 +28,7 @@ export const errorHandler: ErrorRequestHandler = ( return; } - console.error("Internal server error", err); + debugLogger.notice.error("Internal server error", err); res.status(500).json({ error: { message: "Internal server error", diff --git a/js/dev/server.ts b/js/dev/server.ts index 3118e8252..1ae70a61c 100644 --- a/js/dev/server.ts +++ b/js/dev/server.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../src/debug-logger"; import express, { NextFunction, Request, Response } from "express"; import cors from "cors"; import { @@ -165,7 +166,7 @@ export function runDevServer( try { await validateParameters(parameters ?? {}, evaluator.parameters); } catch (e) { - console.error("Error validating parameters", e); + debugLogger.notice.error("Error validating parameters", e); if ( e instanceof z.ZodError || e instanceof ValidationError || @@ -280,7 +281,7 @@ export function runDevServer( res.json(summary.summary); } } catch (e) { - console.error("Error running eval", e); + debugLogger.notice.error("Error running eval", e); if (stream) { res.write( serializeSSEEvent({ diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 2c6d9a6d8..ff61dac11 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import { createUnplugin } from "unplugin"; import { InstrumentationMatcher } from "../orchestrion-js"; import { isAbsolute } from "node:path"; @@ -74,7 +75,9 @@ export const unplugin = createUnplugin((options = {}) => { return transformBundledModule(code, filePath, matcher, browser); } catch (error) { // If transformation fails, warn and return original code - console.warn(`Code transformation failed for ${id}: ${error}`); + debugLogger.notice.warn( + `Code transformation failed for ${id}: ${error}`, + ); return null; } }, diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 6cb397208..67e1c3484 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -21,6 +21,8 @@ * ``` */ +import { debugLogger } from "../../debug-logger"; + import { InstrumentationMatcher } from "../orchestrion-js"; import { getDefaultAutoInstrumentationConfigs } from "../configs/all"; import { type BundlerPluginOptions } from "./plugin"; @@ -81,7 +83,7 @@ function codeTransformerLoader( callback(null, code, inputSourceMap); } } catch (error) { - console.warn( + debugLogger.notice.warn( `[code-transformer-loader] Error transforming ${resourcePath}:`, error, ); diff --git a/js/src/auto-instrumentations/hook.mts b/js/src/auto-instrumentations/hook.mts index 8ea9de355..fb17fb214 100644 --- a/js/src/auto-instrumentations/hook.mts +++ b/js/src/auto-instrumentations/hook.mts @@ -13,6 +13,8 @@ * - CJS modules: Transformed via patching Module._compile */ +import { debugLogger } from "../debug-logger.js"; + import { register } from "node:module"; import { isInstrumentationIntegrationDisabled, @@ -57,20 +59,16 @@ if (!alreadyApplied) { try { patchCommonJS(allConfigs); - if (process.env.DEBUG === "@braintrust*" || process.env.DEBUG === "*") { - console.log( - "[Braintrust] Auto-instrumentation active (ESM + CJS) for:", - allConfigs.map((c) => c.channelName).join(", "), - ); - } + debugLogger.debug( + "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); - } + debugLogger.debug( + "Auto-instrumentation active (ESM only) for:", + allConfigs.map((c) => c.channelName).join(", "), + ); + debugLogger.notice.error("CJS patch failed:", err); } } diff --git a/js/src/auto-instrumentations/loader/cjs-patch.ts b/js/src/auto-instrumentations/loader/cjs-patch.ts index 1a87b87a2..09c73987b 100644 --- a/js/src/auto-instrumentations/loader/cjs-patch.ts +++ b/js/src/auto-instrumentations/loader/cjs-patch.ts @@ -3,6 +3,8 @@ * Patches Module.prototype._compile to transform CommonJS modules at load time. */ +import { debugLogger } from "../../debug-logger"; + import { InstrumentationMatcher, type InstrumentationConfig, @@ -48,7 +50,10 @@ export function patchCommonJS(instrumentations: InstrumentationConfig[]): void { try { source = transformer.transform(source, "unknown").code; } catch (error) { - console.warn(`Error transforming module ${filename}:`, error); + debugLogger.notice.warn( + `Error transforming module ${filename}:`, + error, + ); } } } diff --git a/js/src/auto-instrumentations/loader/esm-hook.mts b/js/src/auto-instrumentations/loader/esm-hook.mts index e966214e0..c4049670c 100644 --- a/js/src/auto-instrumentations/loader/esm-hook.mts +++ b/js/src/auto-instrumentations/loader/esm-hook.mts @@ -3,6 +3,8 @@ * This is used by Node.js --import to transform ES modules at load time. */ +import { debugLogger } from "../../debug-logger.js"; + import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { extname, sep } from "node:path"; @@ -89,7 +91,7 @@ export async function load(url: string, context: any, nextLoad: Function) { ).code; result.shortCircuit = true; } catch (err) { - console.warn(`Error transforming module ${url}:`, err); + debugLogger.notice.warn(`Error transforming module ${url}:`, err); } } return result; diff --git a/js/src/debug-logger.test.ts b/js/src/debug-logger.test.ts index d5a369fe5..88f29c051 100644 --- a/js/src/debug-logger.test.ts +++ b/js/src/debug-logger.test.ts @@ -16,6 +16,7 @@ import { login, } from "./logger"; import { configureNode } from "./node/config"; +import iso from "./isomorph"; configureNode(); @@ -94,6 +95,57 @@ describe("debug logger", () => { ); }); + test("notice messages are shown at warn when no level is configured", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + debugLogger.warn("hidden warning"); + expect(debugLogger.notice.warn("notice warning")).toBe(true); + expect(debugLogger.notice.error("notice error")).toBe(true); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith("[braintrust]", "notice warning"); + expect(errorSpy).toHaveBeenCalledWith("[braintrust]", "notice error"); + }); + + test("notice messages respect an explicit level", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const state = { + getDebugLogLevel: () => "error" as const, + hasDebugLogLevelOverride: () => true, + }; + + const scopedLogger = debugLogger.forState(state); + expect(scopedLogger.notice.warn("notice warning")).toBe(false); + expect(scopedLogger.notice.error("notice error")).toBe(true); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith("[braintrust]", "notice error"); + }); + + test("notice messages are silenced when logging is disabled", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + _exportsForTestingOnly.setInitialTestState(); + const state = _exportsForTestingOnly.simulateLogoutForTests(); + state.setDebugLogLevel(false); + + expect(debugLogger.notice.warn("notice warning")).toBe(false); + expect(debugLogger.notice.error("notice error")).toBe(false); + expect( + debugLogger + .forState({ + getDebugLogLevel: () => undefined, + hasDebugLogLevelOverride: () => true, + }) + .notice.error("scoped notice error"), + ).toBe(false); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); + }); + test("default logger resolves the global SDK state", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); _exportsForTestingOnly.setInitialTestState(); @@ -105,7 +157,8 @@ describe("debug logger", () => { expect(warnSpy).toHaveBeenCalledWith("[braintrust]", "global warning"); }); - test("BRAINTRUST_DEBUG_LOG_LEVEL accepts error, warn, info, and debug", () => { + test("BRAINTRUST_DEBUG_LOG_LEVEL works before platform initialization", () => { + vi.spyOn(iso, "getEnv").mockReturnValue(undefined); process.env.BRAINTRUST_DEBUG_LOG_LEVEL = "error"; expect(getEnvDebugLogLevel()).toBe("error"); @@ -119,6 +172,16 @@ describe("debug logger", () => { expect(getEnvDebugLogLevel()).toBe("debug"); }); + test("BRAINTRUST_DEBUG_LOG_LEVEL fallback tolerates a process shim without env", () => { + vi.spyOn(iso, "getEnv").mockReturnValue(undefined); + vi.stubGlobal("process", {}); + try { + expect(getEnvDebugLogLevel()).toBeUndefined(); + } finally { + vi.unstubAllGlobals(); + } + }); + test("BRAINTRUST_DEBUG_LOG_LEVEL rejects invalid values and warns once", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); process.env.BRAINTRUST_DEBUG_LOG_LEVEL = "true"; diff --git a/js/src/debug-logger.ts b/js/src/debug-logger.ts index 24861c7f4..6b4b6b17c 100644 --- a/js/src/debug-logger.ts +++ b/js/src/debug-logger.ts @@ -9,11 +9,21 @@ type DebugLoggerStateLike = { hasDebugLogLevelOverride?: () => boolean; }; +// Notice methods return whether the message was emitted. +type NoticeLoggerMethods = { + warn: (...args: unknown[]) => boolean; + error: (...args: unknown[]) => boolean; +}; + type DebugLoggerMethods = { info: (...args: unknown[]) => void; debug: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; + // User-facing diagnostics that are shown at "warn" when no debug log level + // has been configured. An explicit level still filters them, and + // `debugLogLevel: false` silences them. + notice: NoticeLoggerMethods; }; const PREFIX = "[braintrust]"; @@ -80,7 +90,13 @@ function parseDebugLogLevelEnv( } export function getEnvDebugLogLevel(): DebugLogLevel | undefined { - return parseDebugLogLevelEnv(iso.getEnv("BRAINTRUST_DEBUG_LOG_LEVEL")); + return parseDebugLogLevelEnv( + iso.getEnv("BRAINTRUST_DEBUG_LOG_LEVEL") ?? + // Loader and bundler diagnostics can run before platform initialization. + (typeof process !== "undefined" && typeof process.env !== "undefined" + ? process.env.BRAINTRUST_DEBUG_LOG_LEVEL + : undefined), + ); } export function setGlobalDebugLogLevel( @@ -103,13 +119,15 @@ export function setDebugLogStateResolver( debugLogStateResolver = resolver; } +// Returns `false` when logging was explicitly disabled and `undefined` when no +// level has been configured. function resolveDebugLogLevel( state?: DebugLoggerStateLike, -): DebugLogLevel | undefined { +): DebugLogLevel | false | undefined { const stateLevel = state?.getDebugLogLevel?.(); const hasStateOverride = state?.hasDebugLogLevelOverride?.() ?? false; if (hasStateOverride) { - return stateLevel; + return stateLevel ?? false; } const globalLevel = @@ -119,7 +137,7 @@ function resolveDebugLogLevel( ] as DebugLogLevel | false | undefined; if (globalLevel !== undefined) { - return globalLevel === false ? undefined : globalLevel; + return globalLevel; } return getEnvDebugLogLevel(); @@ -129,11 +147,12 @@ function emit( method: "info" | "debug" | "warn" | "error", state: DebugLoggerStateLike | undefined, args: unknown[], -) { - const level = resolveDebugLogLevel(state); + defaultLevel?: DebugLogLevel, +): boolean { + const level = resolveDebugLogLevel(state) ?? defaultLevel; if (!level || LOG_LEVEL_PRIORITY[method] > LOG_LEVEL_PRIORITY[level]) { - return; + return false; } if (method === "info") { @@ -149,6 +168,7 @@ function emit( // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.error(PREFIX, ...args); } + return true; } function createDebugLogger(state?: DebugLoggerStateLike): DebugLoggerMethods { @@ -166,6 +186,14 @@ function createDebugLogger(state?: DebugLoggerStateLike): DebugLoggerMethods { error(...args: unknown[]) { emit("error", resolveState(), args); }, + notice: { + warn(...args: unknown[]) { + return emit("warn", resolveState(), args, "warn"); + }, + error(...args: unknown[]) { + return emit("error", resolveState(), args, "warn"); + }, + }, }; } diff --git a/js/src/eval-parameters.ts b/js/src/eval-parameters.ts index 7262cf2b7..433b9991b 100644 --- a/js/src/eval-parameters.ts +++ b/js/src/eval-parameters.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "./debug-logger"; import { z } from "zod/v3"; import * as z4 from "zod/v4"; import Ajv from "ajv"; @@ -125,8 +126,7 @@ function validateParametersWithZod< return [name, schema.parse(value)]; } } catch (e) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error validating parameter", name, e); + debugLogger.notice.error("Error validating parameter", name, e); throw Error( `Invalid parameter '${name}': ${e instanceof Error ? e.message : String(e)}`, ); diff --git a/js/src/framework2.ts b/js/src/framework2.ts index 22fff00ac..458f1d278 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "./debug-logger"; import { _initializeSpanContext } from "./framework"; import type { Trace } from "./trace"; import iso from "./isomorph"; @@ -128,16 +129,16 @@ export class Project { async publish() { if (globalThis._lazy_load) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("publish() is a no-op when running `braintrust push`."); + debugLogger.notice.warn( + "publish() is a no-op when running `braintrust push`.", + ); return; } await login(); const projectMap = new ProjectNameIdMap(); const functionDefinitions: FunctionEvent[] = []; if (this._publishableCodeFunctions.length > 0) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( + debugLogger.notice.warn( "Code functions cannot be published directly. Use `braintrust push` instead.", ); } diff --git a/js/src/instrumentation/core/stream-patcher.test.ts b/js/src/instrumentation/core/stream-patcher.test.ts index d357169f1..195fe7435 100644 --- a/js/src/instrumentation/core/stream-patcher.test.ts +++ b/js/src/instrumentation/core/stream-patcher.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import { describe, it, expect, vi } from "vitest"; import { isAsyncIterable, @@ -396,7 +397,7 @@ describe("patchStreamIfNeeded", () => { }, }); - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(debugLogger, "warn").mockImplementation(() => {}); const onComplete = vi.fn(); const result = patchStreamIfNeeded(stream, { onComplete }); @@ -416,7 +417,7 @@ describe("patchStreamIfNeeded", () => { }, }); - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(debugLogger, "warn").mockImplementation(() => {}); const onComplete = vi.fn(); const result = patchStreamIfNeeded(stream, { onComplete }); @@ -436,7 +437,9 @@ describe("patchStreamIfNeeded", () => { }, }; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = vi + .spyOn(debugLogger, "error") + .mockImplementation(() => {}); const onComplete = vi.fn(() => { throw new Error("Handler error"); }); @@ -464,7 +467,9 @@ describe("patchStreamIfNeeded", () => { }, }; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = vi + .spyOn(debugLogger, "error") + .mockImplementation(() => {}); const onChunk = vi.fn(() => { throw new Error("Chunk handler error"); }); @@ -493,7 +498,9 @@ describe("patchStreamIfNeeded", () => { }, }; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = vi + .spyOn(debugLogger, "error") + .mockImplementation(() => {}); const onError = vi.fn(() => { throw new Error("Error handler error"); }); @@ -678,7 +685,9 @@ describe("wrapStreamResult", () => { }, }; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = vi + .spyOn(debugLogger, "error") + .mockImplementation(() => {}); const processChunks = vi.fn(() => { throw new Error("Process error"); }); @@ -709,7 +718,9 @@ describe("wrapStreamResult", () => { it("should handle errors in onNonStream", () => { const result = { foo: "bar" }; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = vi + .spyOn(debugLogger, "error") + .mockImplementation(() => {}); const processChunks = vi.fn(); const onNonStream = vi.fn(() => { throw new Error("Non-stream error"); diff --git a/js/src/instrumentation/core/stream-patcher.ts b/js/src/instrumentation/core/stream-patcher.ts index d9d77738a..a9aa483e7 100644 --- a/js/src/instrumentation/core/stream-patcher.ts +++ b/js/src/instrumentation/core/stream-patcher.ts @@ -152,8 +152,7 @@ export function patchStreamIfNeeded( // Check if object is extensible (can be patched) if (Object.isFrozen(stream) || Object.isSealed(stream)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( + debugLogger.warn( "Cannot patch frozen/sealed stream. Stream output will not be collected.", ); return stream; @@ -222,8 +221,7 @@ export function patchStreamIfNeeded( try { await options.onComplete(chunks); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onComplete handler:", error); + debugLogger.error("Error in stream onComplete handler:", error); } } } else { @@ -239,8 +237,7 @@ export function patchStreamIfNeeded( try { await options.onChunk(chunk); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onChunk handler:", error); + debugLogger.error("Error in stream onChunk handler:", error); } } } @@ -257,8 +254,10 @@ export function patchStreamIfNeeded( chunks, ); } catch (handlerError) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onError handler:", handlerError); + debugLogger.error( + "Error in stream onError handler:", + handlerError, + ); } } } @@ -289,8 +288,10 @@ export function patchStreamIfNeeded( try { await options.onError(error, chunks); } catch (handlerError) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onError handler:", handlerError); + debugLogger.error( + "Error in stream onError handler:", + handlerError, + ); } } } @@ -305,8 +306,7 @@ export function patchStreamIfNeeded( patchAbortIfPresent(); return stream; } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Failed to patch stream iterator methods:", error); + debugLogger.warn("Failed to patch stream iterator methods:", error); } } @@ -340,8 +340,7 @@ export function patchStreamIfNeeded( try { await options.onComplete(chunks); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onComplete handler:", error); + debugLogger.error("Error in stream onComplete handler:", error); } } } else { @@ -361,8 +360,7 @@ export function patchStreamIfNeeded( try { await options.onChunk(chunk); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onChunk handler:", error); + debugLogger.error("Error in stream onChunk handler:", error); } } } @@ -380,8 +378,10 @@ export function patchStreamIfNeeded( chunks, ); } catch (handlerError) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onError handler:", handlerError); + debugLogger.error( + "Error in stream onError handler:", + handlerError, + ); } } } @@ -416,8 +416,10 @@ export function patchStreamIfNeeded( try { await options.onError(error, chunks); } catch (handlerError) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error in stream onError handler:", handlerError); + debugLogger.error( + "Error in stream onError handler:", + handlerError, + ); } } } @@ -440,8 +442,7 @@ export function patchStreamIfNeeded( return stream; } catch (error) { // If patching fails for any reason, log warning and return original - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Failed to patch stream:", error); + debugLogger.warn("Failed to patch stream:", error); return stream; } } @@ -765,8 +766,7 @@ export function wrapStreamResult( const processed = options.processChunks(chunks); options.onResult(processed); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error processing stream chunks:", error); + debugLogger.error("Error processing stream chunks:", error); if (options.onError) { options.onError( error instanceof Error ? error : new Error(String(error)), @@ -786,8 +786,7 @@ export function wrapStreamResult( : result; options.onResult(processed); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error processing non-stream result:", error); + debugLogger.error("Error processing non-stream result:", error); if (options.onError) { options.onError( error instanceof Error ? error : new Error(String(error)), diff --git a/js/src/instrumentation/plugins/ai-sdk-plugin.ts b/js/src/instrumentation/plugins/ai-sdk-plugin.ts index 267defb73..d45e0edb4 100644 --- a/js/src/instrumentation/plugins/ai-sdk-plugin.ts +++ b/js/src/instrumentation/plugins/ai-sdk-plugin.ts @@ -1906,8 +1906,7 @@ const processContentPart = (part: any): any => { } } } catch (error) { - // eslint-disable-next-line no-restricted-properties - console.warn("Error processing content part:", error); + debugLogger.warn("Error processing content part:", error); } return part; @@ -1956,8 +1955,7 @@ const convertImageToAttachment = ( return image; } } catch (error) { - // eslint-disable-next-line no-restricted-properties - console.warn("Error converting image to attachment:", error); + debugLogger.warn("Error converting image to attachment:", error); } return null; @@ -1996,8 +1994,7 @@ const convertDataToAttachment = ( }); } } catch (error) { - // eslint-disable-next-line no-restricted-properties - console.warn("Error converting data to attachment:", error); + debugLogger.warn("Error converting data to attachment:", error); } return null; diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts b/js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts index f4321935a..17c2f1db9 100644 --- a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts +++ b/js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts @@ -1845,8 +1845,10 @@ export class ClaudeAgentSDKPlugin extends BasePlugin { metadata: filterSerializableOptions(options), }); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error extracting input for Claude Agent SDK:", error); + debugLogger.error( + "Error extracting input for Claude Agent SDK:", + error, + ); } const activeToolSpans = new Map(); @@ -1982,8 +1984,7 @@ export class ClaudeAgentSDKPlugin extends BasePlugin { state.processing = state.processing .then(() => handleStreamMessage(state, message)) .catch((error) => { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( + debugLogger.error( "Error processing Claude Agent SDK stream chunk:", error, ); @@ -2005,8 +2006,10 @@ export class ClaudeAgentSDKPlugin extends BasePlugin { try { state.span.log({ output: result }); } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Error extracting output for Claude Agent SDK:", error); + debugLogger.error( + "Error extracting output for Claude Agent SDK:", + error, + ); } finally { state.span.end(); } diff --git a/js/src/instrumentation/plugins/github-copilot-plugin.ts b/js/src/instrumentation/plugins/github-copilot-plugin.ts index dcc172879..a8e43a085 100644 --- a/js/src/instrumentation/plugins/github-copilot-plugin.ts +++ b/js/src/instrumentation/plugins/github-copilot-plugin.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import { SpanTypeAttribute } from "../../../util/index"; import type { Span } from "../../logger"; import { startSpan as startBaseSpan } from "../../logger"; @@ -650,11 +651,7 @@ function attachSessionEventListener( state.processing = state.processing .then(() => dispatchEvent(state, event)) .catch((err) => { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - "[Braintrust] Error processing GitHub Copilot SDK event:", - err, - ); + debugLogger.error("Error processing GitHub Copilot SDK event:", err); }); }; diff --git a/js/src/instrumentation/registry.test.ts b/js/src/instrumentation/registry.test.ts index fb8707de4..47d2196e9 100644 --- a/js/src/instrumentation/registry.test.ts +++ b/js/src/instrumentation/registry.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { newGlobalInvocationHook } from "../global-instrumentation-hooks"; +import { debugLogger } from "../debug-logger"; vi.mock("../global-instrumentation-hooks", async (importOriginal) => ({ ...(await importOriginal()), newGlobalInvocationHook: vi.fn(), @@ -108,35 +109,36 @@ describe("Plugin Registry", () => { it("should warn if configureInstrumentation is called after enable", () => { const testRegistry = new (registry.constructor as any)(); - const warnSpy = [] as string[]; - const originalWarn = console.warn; - console.warn = (msg: string) => warnSpy.push(msg); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); try { testRegistry.enable(); testRegistry.configure({ integrations: { openai: false } }); - expect(warnSpy.length).toBeGreaterThan(0); - expect(warnSpy[0]).toContain("Cannot configure instrumentation"); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Cannot configure instrumentation"), + ); } finally { - console.warn = originalWarn; + warnSpy.mockRestore(); testRegistry.disable(); } }); it("should allow configuration before enable", () => { const testRegistry = new (registry.constructor as any)(); - const warnSpy = [] as string[]; - const originalWarn = console.warn; - console.warn = (msg: string) => warnSpy.push(msg); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); try { testRegistry.configure({ integrations: { openai: false } }); testRegistry.enable(); - expect(warnSpy.length).toBe(0); + expect(warnSpy).not.toHaveBeenCalled(); } finally { - console.warn = originalWarn; + warnSpy.mockRestore(); testRegistry.disable(); } }); diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 9fbc5b79b..ce15a7fac 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -5,6 +5,8 @@ * Users can disable specific integrations programmatically or via environment variables. */ +import { debugLogger } from "../debug-logger"; + import { BraintrustPlugin } from "./braintrust-plugin"; import iso from "../isomorph"; import { @@ -55,9 +57,8 @@ class PluginRegistry { */ configure(config: InstrumentationConfig): void { if (this.enabled) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - "Braintrust: Cannot configure instrumentation after it has been enabled. " + + debugLogger.notice.warn( + "Cannot configure instrumentation after it has been enabled. " + "Call configureInstrumentation() before importing any AI SDKs.", ); return; diff --git a/js/src/prompt-cache/disk-cache.ts b/js/src/prompt-cache/disk-cache.ts index 8d5dc256c..79ccd385f 100644 --- a/js/src/prompt-cache/disk-cache.ts +++ b/js/src/prompt-cache/disk-cache.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import iso from "../isomorph"; export function canUseDiskCache(): boolean { @@ -96,8 +97,7 @@ export class DiskCache { return undefined; } if (this.logWarnings) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Failed to read from disk cache", e); + debugLogger.warn("Failed to read from disk cache", e); } return undefined; } @@ -122,8 +122,7 @@ export class DiskCache { await this.evictOldestIfFull(); } catch (e) { if (this.logWarnings) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Failed to write to disk cache", e); + debugLogger.warn("Failed to write to disk cache", e); } return; } diff --git a/js/src/span-customizer.test.ts b/js/src/span-customizer.test.ts index 954d46c7a..fa3ad721f 100644 --- a/js/src/span-customizer.test.ts +++ b/js/src/span-customizer.test.ts @@ -48,6 +48,7 @@ describe("onSpanExport", () => { let memoryLogger: TestBackgroundLogger; beforeEach(async () => { + vi.stubEnv("BRAINTRUST_DEBUG_LOG_LEVEL", "error"); registry.disable(); await _exportsForTestingOnly.simulateLoginForTests(); memoryLogger = _exportsForTestingOnly.useTestBackgroundLogger(); @@ -833,6 +834,34 @@ describe("onSpanExport", () => { } }); + test("logs the failure once logging is enabled after a silenced failure", () => { + const errorLog = vi.spyOn(console, "error").mockImplementation(() => {}); + const state = _internalGetGlobalState(); + configureInstrumentation({ + spanCustomizers: [ + { + onSpanExport(): SpanExportData { + throw new Error("private exception detail"); + }, + }, + ], + }); + + state.setDebugLogLevel(false); + customizeSpanExport({ id: "silenced" }); + expect(errorLog).not.toHaveBeenCalled(); + + state.setDebugLogLevel("error"); + customizeSpanExport({ id: "visible" }); + customizeSpanExport({ id: "deduplicated" }); + expect(errorLog.mock.calls).toEqual([ + [ + "[braintrust]", + "Span export customizer failed; exporting only span identity and duration.", + ], + ]); + }); + test("logs safely once across time, registrations, and SDK module copies", async () => { vi.useFakeTimers({ toFake: ["Date"] }); const errorLog = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -863,7 +892,8 @@ describe("onSpanExport", () => { expect(errorLog).toHaveBeenCalledTimes(1); expect(errorLog.mock.calls).toEqual([ [ - "[braintrust] Span export customizer failed; exporting only span identity and duration.", + "[braintrust]", + "Span export customizer failed; exporting only span identity and duration.", ], ]); }); diff --git a/js/src/span-customizer.ts b/js/src/span-customizer.ts index a86eda4df..802d4c829 100644 --- a/js/src/span-customizer.ts +++ b/js/src/span-customizer.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "./debug-logger"; import { ARRAY_DELETE_FIELD, ID_FIELD, @@ -152,12 +153,15 @@ function restoreProtocolFields( function registerCustomizerFailure(): void { if (shared[CUSTOMIZER_FAILURE_LOGGED_KEY]) return; - shared[CUSTOMIZER_FAILURE_LOGGED_KEY] = true; // Never log the exception or record: either can contain sensitive data. - // eslint-disable-next-line no-restricted-properties -- intentional always-visible export failure. - console.error( - "[braintrust] Span export customizer failed; exporting only span identity and duration.", - ); + // Only mark as logged once emitted so enabling logging later still surfaces it. + if ( + debugLogger.notice.error( + "Span export customizer failed; exporting only span identity and duration.", + ) + ) { + shared[CUSTOMIZER_FAILURE_LOGGED_KEY] = true; + } } export function resetSpanCustomizerFailureLoggingForTests(): void { diff --git a/js/src/template/registry.ts b/js/src/template/registry.ts index 6ebcace61..d9744e8a4 100644 --- a/js/src/template/registry.ts +++ b/js/src/template/registry.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { mustachePlugin } from "./plugins/mustache"; export type TemplateFormat = "mustache" | "nunjucks" | "none"; @@ -67,8 +68,7 @@ class TemplatePluginRegistry { register(plugin: TemplateRendererPlugin): void { if (this.plugins.has(plugin.name)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( + debugLogger.warn( `Template plugin '${plugin.name}' already registered, overwriting`, ); } diff --git a/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts b/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts index 705048346..f28764813 100644 --- a/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts +++ b/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../../debug-logger"; /* eslint-disable @typescript-eslint/consistent-type-assertions */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { startSpan } from "../../../logger"; @@ -29,8 +30,7 @@ export function wrapAISDKModel(model: T): T { ) { return new BraintrustLanguageModelWrapper(m) as any as T; } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported AI SDK model. Not wrapping."); + debugLogger.notice.warn("Unsupported AI SDK model. Not wrapping."); return model; } } diff --git a/js/src/wrappers/ai-sdk/telemetry.ts b/js/src/wrappers/ai-sdk/telemetry.ts index 08f2989b8..12f60a9f2 100644 --- a/js/src/wrappers/ai-sdk/telemetry.ts +++ b/js/src/wrappers/ai-sdk/telemetry.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import { isAutoInstrumentationSuppressed } from "../../instrumentation/auto-instrumentation-suppression"; import { SpanTypeAttribute, @@ -93,8 +94,7 @@ export function braintrustAISDKTelemetry(): any { try { callback(); } catch (error) { - // eslint-disable-next-line no-restricted-properties - console.error(`Error in Braintrust AI SDK telemetry ${name}:`, error); + debugLogger.error(`Error in Braintrust AI SDK telemetry ${name}:`, error); } }; diff --git a/js/src/wrappers/anthropic.ts b/js/src/wrappers/anthropic.ts index 43d1b6471..e98f9b12f 100644 --- a/js/src/wrappers/anthropic.ts +++ b/js/src/wrappers/anthropic.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { anthropicChannels } from "../instrumentation/plugins/anthropic-interceptors"; import { TypedApplyProxy } from "../typed-instrumentation-helpers"; import type { @@ -38,8 +39,7 @@ export function wrapAnthropic(anthropic: T): T { return anthropicProxy(au as AnthropicClient) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Anthropic library. Not wrapping."); + debugLogger.notice.warn("Unsupported Anthropic library. Not wrapping."); return anthropic; } diff --git a/js/src/wrappers/bedrock-runtime.test.ts b/js/src/wrappers/bedrock-runtime.test.ts index e2a8d606a..4da0d27c5 100644 --- a/js/src/wrappers/bedrock-runtime.test.ts +++ b/js/src/wrappers/bedrock-runtime.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, beforeAll, @@ -56,7 +57,9 @@ describe("bedrock runtime wrapper", () => { }); test("returns original object for unsupported clients", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const invalid = { foo: "bar" }; expect(wrapBedrockRuntime(invalid)).toBe(invalid); diff --git a/js/src/wrappers/bedrock-runtime.ts b/js/src/wrappers/bedrock-runtime.ts index 38efbf3ee..3919553cc 100644 --- a/js/src/wrappers/bedrock-runtime.ts +++ b/js/src/wrappers/bedrock-runtime.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { runWithAutoInstrumentationSuppressed } from "../instrumentation/auto-instrumentation-suppression"; import { bedrockRuntimeChannels } from "../instrumentation/plugins/bedrock-runtime-interceptors"; import { @@ -17,8 +18,7 @@ export function wrapBedrockRuntime(client: T): T { return bedrockRuntimeProxy(client) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving wrapper warning behavior. - console.warn("Unsupported Bedrock Runtime library. Not wrapping."); + debugLogger.notice.warn("Unsupported Bedrock Runtime library. Not wrapping."); return client; } diff --git a/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts b/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts index ac80bc297..4ed523dd3 100644 --- a/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts +++ b/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import { claudeAgentSDKChannels } from "../../instrumentation/plugins/claude-agent-sdk-interceptors"; import { CLAUDE_AGENT_SDK_SKIP_LOCAL_TOOL_HOOKS_OPTION } from "../../instrumentation/plugins/claude-agent-sdk-instrumentation-constants"; import { wrapLocalClaudeToolHandler } from "../../instrumentation/plugins/claude-agent-sdk-local-tool-spans"; @@ -31,8 +32,7 @@ export function wrapClaudeAgentSDK(sdk: T): T { return claudeAgentSDKProxy(s as ClaudeAgentSDKModule) as unknown as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Claude Agent SDK. Not wrapping."); + debugLogger.notice.warn("Unsupported Claude Agent SDK. Not wrapping."); return sdk; } diff --git a/js/src/wrappers/cloudflare-agent.ts b/js/src/wrappers/cloudflare-agent.ts index 8825113a3..4930a5185 100644 --- a/js/src/wrappers/cloudflare-agent.ts +++ b/js/src/wrappers/cloudflare-agent.ts @@ -86,5 +86,7 @@ function isObjectLike(value: unknown): value is object { } function warnUnsupportedAgent(): void { - debugLogger.warn("Unsupported Cloudflare Agents Agent class. Not wrapping."); + debugLogger.notice.warn( + "Unsupported Cloudflare Agents Agent class. Not wrapping.", + ); } diff --git a/js/src/wrappers/cloudflare-ai-chat.ts b/js/src/wrappers/cloudflare-ai-chat.ts index 87df27775..4023bbb49 100644 --- a/js/src/wrappers/cloudflare-ai-chat.ts +++ b/js/src/wrappers/cloudflare-ai-chat.ts @@ -30,7 +30,7 @@ export function wrapCloudflareAIChat(module: T): T { } if (typeof AIChatAgent !== "function") { - debugLogger.warn( + debugLogger.notice.warn( "Unsupported @cloudflare/ai-chat module. AIChatAgent was not found; not wrapping.", ); return module; diff --git a/js/src/wrappers/cohere.test.ts b/js/src/wrappers/cohere.test.ts index 5c585b948..9f580d166 100644 --- a/js/src/wrappers/cohere.test.ts +++ b/js/src/wrappers/cohere.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, beforeAll, @@ -40,7 +41,9 @@ describe("cohere wrapper", () => { }); test("returns original object for unsupported clients", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const invalid = { foo: "bar" }; expect(wrapCohere(invalid)).toBe(invalid); diff --git a/js/src/wrappers/cohere.ts b/js/src/wrappers/cohere.ts index 7f3d84405..a91a8b094 100644 --- a/js/src/wrappers/cohere.ts +++ b/js/src/wrappers/cohere.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { cohereChannels } from "../instrumentation/plugins/cohere-interceptors"; import type { CohereChatRequest, @@ -19,8 +20,7 @@ export function wrapCohere(cohere: T): T { return cohereProxy(cohere) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Cohere library. Not wrapping."); + debugLogger.notice.warn("Unsupported Cohere library. Not wrapping."); return cohere; } diff --git a/js/src/wrappers/cursor-sdk.test.ts b/js/src/wrappers/cursor-sdk.test.ts index 8a63363a9..b260e4a1d 100644 --- a/js/src/wrappers/cursor-sdk.test.ts +++ b/js/src/wrappers/cursor-sdk.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, describe, expect, it, vi } from "vitest"; const { invoke } = vi.hoisted(() => ({ @@ -23,7 +24,9 @@ describe("wrapCursorSDK", () => { }); it("returns invalid modules unchanged", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const sdk = { Cursor: {} }; expect(wrapCursorSDK(sdk)).toBe(sdk); diff --git a/js/src/wrappers/cursor-sdk.ts b/js/src/wrappers/cursor-sdk.ts index 2188066e0..a2ec37d60 100644 --- a/js/src/wrappers/cursor-sdk.ts +++ b/js/src/wrappers/cursor-sdk.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { cursorSDKChannels } from "../instrumentation/plugins/cursor-sdk-interceptors"; import type { CursorSDKAgent, @@ -21,8 +22,7 @@ export function wrapCursorSDK(sdk: T): T { const maybeSDK = sdk as Record; if (!maybeSDK.Agent || typeof maybeSDK.Agent !== "function") { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Cursor SDK. Not wrapping."); + debugLogger.notice.warn("Unsupported Cursor SDK. Not wrapping."); return sdk; } diff --git a/js/src/wrappers/elevenlabs.ts b/js/src/wrappers/elevenlabs.ts index 01700532c..b106e3422 100644 --- a/js/src/wrappers/elevenlabs.ts +++ b/js/src/wrappers/elevenlabs.ts @@ -30,7 +30,7 @@ export function wrapElevenLabs(client: T): T { !isObject(client.textToSpeech) || typeof client.textToSpeech.convert !== "function" ) { - debugLogger.warn("Unsupported ElevenLabs client. Not wrapping."); + debugLogger.notice.warn("Unsupported ElevenLabs client. Not wrapping."); return client; } const cached = clients.get(client); diff --git a/js/src/wrappers/genkit.ts b/js/src/wrappers/genkit.ts index eea037b25..47cbcd384 100644 --- a/js/src/wrappers/genkit.ts +++ b/js/src/wrappers/genkit.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { genkitChannels } from "../instrumentation/plugins/genkit-interceptors"; import type { GenkitAction, @@ -36,8 +37,7 @@ export function wrapGenkit(genkit: T): T { return wrapGenkitModule(genkit) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Genkit object. Not wrapping."); + debugLogger.notice.warn("Unsupported Genkit object. Not wrapping."); return genkit; } diff --git a/js/src/wrappers/github-copilot.ts b/js/src/wrappers/github-copilot.ts index 249f9b71c..4faf0fd43 100644 --- a/js/src/wrappers/github-copilot.ts +++ b/js/src/wrappers/github-copilot.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { gitHubCopilotChannels } from "../instrumentation/plugins/github-copilot-interceptors"; import type { GitHubCopilotClient, @@ -37,9 +38,8 @@ function isGitHubCopilotClient(value: unknown): value is GitHubCopilotClient { */ export function wrapCopilotClient(client: T): T { if (!isGitHubCopilotClient(client)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - "[Braintrust] wrapCopilotClient: argument does not look like a CopilotClient. Not wrapping.", + debugLogger.notice.warn( + "wrapCopilotClient: argument does not look like a CopilotClient. Not wrapping.", ); return client; } diff --git a/js/src/wrappers/google-adk.test.ts b/js/src/wrappers/google-adk.test.ts index 77256f6ba..67de8a16b 100644 --- a/js/src/wrappers/google-adk.test.ts +++ b/js/src/wrappers/google-adk.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { describe, it, expect, vi, afterEach } from "vitest"; const { invoke } = vi.hoisted(() => ({ @@ -23,7 +24,9 @@ describe("wrapGoogleADK", () => { }); it("should return the module unchanged if invalid", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const result = wrapGoogleADK(null as any); expect(result).toBeNull(); @@ -35,7 +38,9 @@ describe("wrapGoogleADK", () => { }); it("should return module unchanged if no Runner or LlmAgent found", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const fakeModule = { SomeOtherClass: class {} }; const result = wrapGoogleADK(fakeModule as any); diff --git a/js/src/wrappers/google-adk.ts b/js/src/wrappers/google-adk.ts index 4a22203f8..875ce4ca4 100644 --- a/js/src/wrappers/google-adk.ts +++ b/js/src/wrappers/google-adk.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { googleADKChannels } from "../instrumentation/plugins/google-adk-interceptors"; import type { GoogleADKBaseAgent, @@ -41,14 +42,12 @@ import type { */ export function wrapGoogleADK>(adkModule: T): T { if (!adkModule || typeof adkModule !== "object") { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Invalid Google ADK module. Not wrapping."); + debugLogger.notice.warn("Invalid Google ADK module. Not wrapping."); return adkModule; } if (!("Runner" in adkModule) && !("LlmAgent" in adkModule)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( + debugLogger.notice.warn( "Runner or LlmAgent class not found in module. Not wrapping. Make sure you're passing the module itself (import * as adk from '@google/adk').", ); return adkModule; diff --git a/js/src/wrappers/google-genai.ts b/js/src/wrappers/google-genai.ts index b4e0c5bb7..f9ed4917c 100644 --- a/js/src/wrappers/google-genai.ts +++ b/js/src/wrappers/google-genai.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { googleGenAIChannels } from "../instrumentation/plugins/google-genai-interceptors"; import { isObject } from "../util"; import type { @@ -37,14 +38,12 @@ export function wrapGoogleGenAI>( googleGenAI: T, ): T { if (!googleGenAI || typeof googleGenAI !== "object") { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Invalid Google GenAI module. Not wrapping."); + debugLogger.notice.warn("Invalid Google GenAI module. Not wrapping."); return googleGenAI; } if (!("GoogleGenAI" in googleGenAI)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( + debugLogger.notice.warn( "GoogleGenAI class not found in module. Not wrapping. Make sure you're passing the module itself (import * as googleGenAI from '@google/genai').", ); return googleGenAI; diff --git a/js/src/wrappers/groq.test.ts b/js/src/wrappers/groq.test.ts index d44daf491..88a6c0f95 100644 --- a/js/src/wrappers/groq.test.ts +++ b/js/src/wrappers/groq.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, beforeAll, @@ -41,7 +42,9 @@ describe("groq wrapper", () => { }); test("returns original object for unsupported clients", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const invalid = { foo: "bar" }; expect(wrapGroq(invalid)).toBe(invalid); diff --git a/js/src/wrappers/groq.ts b/js/src/wrappers/groq.ts index 6f71b7e76..488efdd6d 100644 --- a/js/src/wrappers/groq.ts +++ b/js/src/wrappers/groq.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { groqChannels } from "../instrumentation/plugins/groq-interceptors"; import type { GroqAudio, @@ -22,8 +23,7 @@ export function wrapGroq(groq: T): T { return groqProxy(groq) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Groq library. Not wrapping."); + debugLogger.notice.warn("Unsupported Groq library. Not wrapping."); return groq; } diff --git a/js/src/wrappers/huggingface.ts b/js/src/wrappers/huggingface.ts index 80272ac56..c01e0756c 100644 --- a/js/src/wrappers/huggingface.ts +++ b/js/src/wrappers/huggingface.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { isObject } from "../../util"; import { huggingFaceChannels } from "../instrumentation/plugins/huggingface-interceptors"; import type { @@ -51,8 +52,9 @@ export function wrapHuggingFace(huggingFace: unknown): unknown { return clientProxy(huggingFace); } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported HuggingFace Inference SDK. Not wrapping."); + debugLogger.notice.warn( + "Unsupported HuggingFace Inference SDK. Not wrapping.", + ); return huggingFace; } diff --git a/js/src/wrappers/langgraph-sdk.ts b/js/src/wrappers/langgraph-sdk.ts index 5c82a13b6..6c33b6487 100644 --- a/js/src/wrappers/langgraph-sdk.ts +++ b/js/src/wrappers/langgraph-sdk.ts @@ -13,7 +13,7 @@ export function wrapLangGraphSDK(client: T): T { !isObject(runsClient) || !["wait", "stream"].every((key) => typeof runsClient[key] === "function") ) { - debugLogger.warn("Unsupported LangGraph SDK client. Not wrapping."); + debugLogger.notice.warn("Unsupported LangGraph SDK client. Not wrapping."); return client; } const sdk = client as unknown as LangGraphSDKClient; diff --git a/js/src/wrappers/langsmith.ts b/js/src/wrappers/langsmith.ts index b5d0ab37f..5213d5366 100644 --- a/js/src/wrappers/langsmith.ts +++ b/js/src/wrappers/langsmith.ts @@ -74,8 +74,7 @@ function wrapNamespaceExport( return namespace; } if (typeof candidate[exportName] !== "function") { - // eslint-disable-next-line no-restricted-properties -- public wrapper diagnostics follow existing wrapper behavior. - console.warn( + debugLogger.notice.warn( `Unsupported LangSmith ${exportName} namespace. Not wrapping.`, ); return namespace; diff --git a/js/src/wrappers/mistral.ts b/js/src/wrappers/mistral.ts index 9bdcece10..af5003074 100644 --- a/js/src/wrappers/mistral.ts +++ b/js/src/wrappers/mistral.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { mistralChannels } from "../instrumentation/plugins/mistral-interceptors"; import type { MistralAgents, @@ -31,8 +32,7 @@ export function wrapMistral(mistral: T): T { return mistralProxy(mistral) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Mistral library. Not wrapping."); + debugLogger.notice.warn("Unsupported Mistral library. Not wrapping."); return mistral; } diff --git a/js/src/wrappers/oai.ts b/js/src/wrappers/oai.ts index 787517a41..d49800de9 100644 --- a/js/src/wrappers/oai.ts +++ b/js/src/wrappers/oai.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import type { OpenAIMediaClient, OpenAIMediaMethod, @@ -68,8 +69,9 @@ export function wrapOpenAI(openai: T): T { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return wrapOpenAIv4(typedOpenAI) as T; } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported OpenAI library (potentially v3). Not wrapping."); + debugLogger.notice.warn( + "Unsupported OpenAI library (potentially v3). Not wrapping.", + ); return openai; } } diff --git a/js/src/wrappers/ollama.test.ts b/js/src/wrappers/ollama.test.ts index 9b368166e..2db63e5eb 100644 --- a/js/src/wrappers/ollama.test.ts +++ b/js/src/wrappers/ollama.test.ts @@ -52,7 +52,9 @@ describe("wrapOllama", () => { it("returns unsupported objects unchanged", () => { const client = {}; - const warn = vi.spyOn(debugLogger, "warn").mockImplementation(() => {}); + const warn = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); expect(wrapOllama(client)).toBe(client); expect(warn).toHaveBeenCalledWith( diff --git a/js/src/wrappers/ollama.ts b/js/src/wrappers/ollama.ts index 8f20da200..c5b9a5faa 100644 --- a/js/src/wrappers/ollama.ts +++ b/js/src/wrappers/ollama.ts @@ -20,7 +20,7 @@ export function wrapOllama(ollama: T): T { return ollamaProxy(ollama) as T; } - debugLogger.warn("Unsupported Ollama library. Not wrapping."); + debugLogger.notice.warn("Unsupported Ollama library. Not wrapping."); return ollama; } diff --git a/js/src/wrappers/openai-codex.ts b/js/src/wrappers/openai-codex.ts index 901bc855e..ced9c7708 100644 --- a/js/src/wrappers/openai-codex.ts +++ b/js/src/wrappers/openai-codex.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { openAICodexChannels } from "../instrumentation/plugins/openai-codex-interceptors"; import type { OpenAICodexClass, @@ -28,8 +29,7 @@ export function wrapOpenAICodexSDK(sdk: T): T { } if (!maybeSDK.Codex || typeof maybeSDK.Codex !== "function") { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported OpenAI Codex SDK. Not wrapping."); + debugLogger.notice.warn("Unsupported OpenAI Codex SDK. Not wrapping."); return sdk; } diff --git a/js/src/wrappers/openrouter-agent.test.ts b/js/src/wrappers/openrouter-agent.test.ts index 069960ca3..c84a48333 100644 --- a/js/src/wrappers/openrouter-agent.test.ts +++ b/js/src/wrappers/openrouter-agent.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, describe, expect, it, vi } from "vitest"; import { wrapOpenRouterAgent } from "./openrouter-agent"; import { openRouterAgentChannels } from "../instrumentation/plugins/openrouter-agent-interceptors"; @@ -8,7 +9,9 @@ describe("wrapOpenRouterAgent", () => { }); it("returns the original value and warns for unsupported inputs", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const input = { notCallModel: true }; expect(wrapOpenRouterAgent(input as object)).toBe(input); diff --git a/js/src/wrappers/openrouter-agent.ts b/js/src/wrappers/openrouter-agent.ts index 0df5ed972..dd077e704 100644 --- a/js/src/wrappers/openrouter-agent.ts +++ b/js/src/wrappers/openrouter-agent.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { openRouterAgentChannels } from "../instrumentation/plugins/openrouter-agent-interceptors"; import type { OpenRouterAgentCallModelRequest, @@ -19,8 +20,9 @@ export function wrapOpenRouterAgent(agent: T): T { return openRouterAgentProxy(candidate as OpenRouterAgentClient) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported OpenRouter Agent library. Not wrapping."); + debugLogger.notice.warn( + "Unsupported OpenRouter Agent library. Not wrapping.", + ); return agent; } diff --git a/js/src/wrappers/openrouter.test.ts b/js/src/wrappers/openrouter.test.ts index 4ee88ea24..50b0bdbb3 100644 --- a/js/src/wrappers/openrouter.test.ts +++ b/js/src/wrappers/openrouter.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, beforeAll, @@ -49,7 +50,9 @@ describe("openrouter wrapper", () => { }); test("returns the original object for unsupported clients", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const invalid = { foo: "bar" }; expect(wrapOpenRouter(invalid)).toBe(invalid); diff --git a/js/src/wrappers/openrouter.ts b/js/src/wrappers/openrouter.ts index 7bc2abd06..c3f6278be 100644 --- a/js/src/wrappers/openrouter.ts +++ b/js/src/wrappers/openrouter.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { openRouterChannels } from "../instrumentation/plugins/openrouter-interceptors"; import type { OpenRouterBeta, @@ -43,8 +44,7 @@ export function wrapOpenRouter(openrouter: T): T { return openRouterProxy(or as OpenRouterClient) as T; } - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported OpenRouter library. Not wrapping."); + debugLogger.notice.warn("Unsupported OpenRouter library. Not wrapping."); return openrouter; } diff --git a/js/src/wrappers/pi-coding-agent.test.ts b/js/src/wrappers/pi-coding-agent.test.ts index 7c6438fb1..678b6481c 100644 --- a/js/src/wrappers/pi-coding-agent.test.ts +++ b/js/src/wrappers/pi-coding-agent.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, describe, expect, it, vi } from "vitest"; const { invoke } = vi.hoisted(() => ({ @@ -23,7 +24,9 @@ describe("wrapPiCodingAgentSDK", () => { }); it("returns invalid modules unchanged", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const sdk = { Session: class {} }; expect(wrapPiCodingAgentSDK(sdk)).toBe(sdk); diff --git a/js/src/wrappers/pi-coding-agent.ts b/js/src/wrappers/pi-coding-agent.ts index c922e7ed4..c7051b1b2 100644 --- a/js/src/wrappers/pi-coding-agent.ts +++ b/js/src/wrappers/pi-coding-agent.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { piCodingAgentChannels } from "../instrumentation/plugins/pi-coding-agent-interceptors"; import type { PiAgentSession, @@ -19,8 +20,7 @@ export function wrapPiCodingAgentSDK(sdk: T): T { const maybeSDK = sdk as Record; if (!maybeSDK.AgentSession || typeof maybeSDK.AgentSession !== "function") { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Pi Coding Agent SDK. Not wrapping."); + debugLogger.notice.warn("Unsupported Pi Coding Agent SDK. Not wrapping."); return sdk; } @@ -39,8 +39,7 @@ function patchAgentSessionClass(AgentSession: PiAgentSessionClass): void { const descriptor = Object.getOwnPropertyDescriptor(prototype, "prompt"); if (!descriptor || typeof descriptor.value !== "function") { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Pi Coding Agent SDK. Not wrapping."); + debugLogger.notice.warn("Unsupported Pi Coding Agent SDK. Not wrapping."); return; } diff --git a/js/src/wrappers/shared/scorers.ts b/js/src/wrappers/shared/scorers.ts index b3c11ee45..d627557e6 100644 --- a/js/src/wrappers/shared/scorers.ts +++ b/js/src/wrappers/shared/scorers.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import type { Span } from "../../logger"; import type { Score } from "../../../util/score"; import type { ScorerFunction } from "./types"; @@ -45,8 +46,7 @@ export async function runScorers(args: { } catch (scorerError) { // Log scorer error but don't fail the test — use metadata instead // of top-level error field to avoid marking the span as errored - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Braintrust: Scorer failed:", scorerError); + debugLogger.notice.warn("Scorer failed:", scorerError); const errorStr = scorerError instanceof Error ? `${scorerError.message}\n\n${scorerError.stack || ""}` diff --git a/js/src/wrappers/strands-agent-sdk.test.ts b/js/src/wrappers/strands-agent-sdk.test.ts index 5300ea3ae..82ab652ed 100644 --- a/js/src/wrappers/strands-agent-sdk.test.ts +++ b/js/src/wrappers/strands-agent-sdk.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, describe, expect, it, vi } from "vitest"; const { invoke } = vi.hoisted(() => ({ @@ -23,7 +24,9 @@ describe("wrapStrandsAgentSDK", () => { }); it("returns invalid modules unchanged", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const sdk = { AgentSession: class {} }; expect(wrapStrandsAgentSDK(sdk)).toBe(sdk); diff --git a/js/src/wrappers/strands-agent-sdk.ts b/js/src/wrappers/strands-agent-sdk.ts index fe61102b5..90b6c28c4 100644 --- a/js/src/wrappers/strands-agent-sdk.ts +++ b/js/src/wrappers/strands-agent-sdk.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { strandsAgentSDKChannels } from "../instrumentation/plugins/strands-agent-sdk-interceptors"; import type { StrandsAgent, @@ -35,8 +36,7 @@ export function wrapStrandsAgentSDK(sdk: T): T { typeof maybeSDK.Graph !== "function" && typeof maybeSDK.Swarm !== "function" ) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Strands Agent SDK. Not wrapping."); + debugLogger.notice.warn("Unsupported Strands Agent SDK. Not wrapping."); return sdk; } diff --git a/js/src/wrappers/typesafe.test.ts b/js/src/wrappers/typesafe.test.ts index 7aaa287d9..0d86402a5 100644 --- a/js/src/wrappers/typesafe.test.ts +++ b/js/src/wrappers/typesafe.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, beforeAll, @@ -112,7 +113,9 @@ describe("TypeSafe wrapper", () => { }); it("returns unsupported clients unchanged", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const unsupported = { models: {} }; expect(wrapTypeSafe(unsupported)).toBe(unsupported); diff --git a/js/src/wrappers/typesafe.ts b/js/src/wrappers/typesafe.ts index 9daafc704..3b639dd32 100644 --- a/js/src/wrappers/typesafe.ts +++ b/js/src/wrappers/typesafe.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { typeSafeChannels } from "../instrumentation/plugins/typesafe-interceptors"; import type { TypeSafeClient, @@ -8,8 +9,7 @@ import type { export function wrapTypeSafe(client: T): T; export function wrapTypeSafe(client: unknown): unknown { if (!isSupportedTypeSafeClient(client)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported TypeSafe AI library. Not wrapping."); + debugLogger.notice.warn("Unsupported TypeSafe AI library. Not wrapping."); return client; } diff --git a/js/src/wrappers/vitest/index.ts b/js/src/wrappers/vitest/index.ts index 6e2c24fdd..da2fdae12 100644 --- a/js/src/wrappers/vitest/index.ts +++ b/js/src/wrappers/vitest/index.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../../debug-logger"; import { wrapTest, wrapDescribe, getExperimentContext } from "./wrapper"; import { wrapExpect } from "./expect-wrapper"; import { summarizeAndFlush } from "../shared/flush"; @@ -98,9 +99,8 @@ export function wrapVitest< flushExperiment: async (options?: { displaySummary?: boolean }) => { const ctx = getExperimentContext(); if (!ctx) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - "Braintrust: No experiment context found. Make sure you're using bt.describe() and calling flushExperiment() within an afterAll() hook.", + debugLogger.notice.warn( + "No experiment context found. Make sure you're using bt.describe() and calling flushExperiment() within an afterAll() hook.", ); return; } diff --git a/js/src/wrappers/voyageai.test.ts b/js/src/wrappers/voyageai.test.ts index e9a1f9e19..323a0bc2f 100644 --- a/js/src/wrappers/voyageai.test.ts +++ b/js/src/wrappers/voyageai.test.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { afterEach, beforeAll, @@ -40,7 +41,9 @@ describe("Voyage AI wrapper", () => { }); it("returns unsupported clients unchanged", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi + .spyOn(debugLogger.notice, "warn") + .mockImplementation(() => true); const unsupported = { tokenize: vi.fn() }; expect(wrapVoyageAI(unsupported)).toBe(unsupported); diff --git a/js/src/wrappers/voyageai.ts b/js/src/wrappers/voyageai.ts index 22b04aada..b36e17a2b 100644 --- a/js/src/wrappers/voyageai.ts +++ b/js/src/wrappers/voyageai.ts @@ -1,3 +1,4 @@ +import { debugLogger } from "../debug-logger"; import { voyageAIChannels } from "../instrumentation/plugins/voyageai-interceptors"; import type { VoyageAIClient, @@ -13,8 +14,7 @@ import type { */ export function wrapVoyageAI(client: T): T { if (!isSupportedVoyageAIClient(client)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported Voyage AI library. Not wrapping."); + debugLogger.notice.warn("Unsupported Voyage AI library. Not wrapping."); return client; }