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
3 changes: 2 additions & 1 deletion js/dev/authorize.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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"));
}
};
Expand Down
3 changes: 2 additions & 1 deletion js/dev/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions js/dev/server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { debugLogger } from "../src/debug-logger";
import express, { NextFunction, Request, Response } from "express";
import cors from "cors";
import {
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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({
Expand Down
5 changes: 4 additions & 1 deletion js/src/auto-instrumentations/bundler/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { debugLogger } from "../../debug-logger";
import { createUnplugin } from "unplugin";
import { InstrumentationMatcher } from "../orchestrion-js";
import { isAbsolute } from "node:path";
Expand Down Expand Up @@ -74,7 +75,9 @@ export const unplugin = createUnplugin<BundlerPluginOptions>((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;
}
},
Expand Down
4 changes: 3 additions & 1 deletion js/src/auto-instrumentations/bundler/webpack-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
* ```
*/

import { debugLogger } from "../../debug-logger";

import { InstrumentationMatcher } from "../orchestrion-js";
import { getDefaultAutoInstrumentationConfigs } from "../configs/all";
import { type BundlerPluginOptions } from "./plugin";
Expand Down Expand Up @@ -81,7 +83,7 @@ function codeTransformerLoader(
callback(null, code, inputSourceMap);
}
} catch (error) {
console.warn(
debugLogger.notice.warn(
`[code-transformer-loader] Error transforming ${resourcePath}:`,
error,
);
Expand Down
24 changes: 11 additions & 13 deletions js/src/auto-instrumentations/hook.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* - CJS modules: Transformed via patching Module._compile
*/

import { debugLogger } from "../debug-logger.js";

import { register } from "node:module";
import {
isInstrumentationIntegrationDisabled,
Expand Down Expand Up @@ -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);
}
}
7 changes: 6 additions & 1 deletion js/src/auto-instrumentations/loader/cjs-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Patches Module.prototype._compile to transform CommonJS modules at load time.
*/

import { debugLogger } from "../../debug-logger";

import {
InstrumentationMatcher,
type InstrumentationConfig,
Expand Down Expand Up @@ -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,
);
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion js/src/auto-instrumentations/loader/esm-hook.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
65 changes: 64 additions & 1 deletion js/src/debug-logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
login,
} from "./logger";
import { configureNode } from "./node/config";
import iso from "./isomorph";

configureNode();

Expand Down Expand Up @@ -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();
Expand All @@ -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");

Expand All @@ -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";
Expand Down
42 changes: 35 additions & 7 deletions js/src/debug-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]";
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard the process.env fallback

In browser or edge runtimes that expose a partial process shim without process.env, this fallback throws while evaluating process.env.BRAINTRUST_DEBUG_LOG_LEVEL. browser/config.ts explicitly supports this environment by having iso.getEnv return undefined when process.env is absent, but the new fallback defeats that guard; because browser initialization constructs BraintrustState, which calls getEnvDebugLogLevel, importing the SDK can now fail before use. Check that process.env exists before dereferencing it.

Useful? React with 👍 / 👎.

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.

Fixed in 6815ef2: the fallback now checks typeof process.env !== "undefined" (matching browser/config.ts), with a test for a process shim without env.

: undefined),
);
}

export function setGlobalDebugLogLevel(
Expand All @@ -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 =
Expand All @@ -119,7 +137,7 @@ function resolveDebugLogLevel(
] as DebugLogLevel | false | undefined;

if (globalLevel !== undefined) {
return globalLevel === false ? undefined : globalLevel;
return globalLevel;
}

return getEnvDebugLogLevel();
Expand All @@ -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") {
Expand All @@ -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 {
Expand All @@ -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");
},
},
};
}

Expand Down
4 changes: 2 additions & 2 deletions js/src/eval-parameters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { debugLogger } from "./debug-logger";
import { z } from "zod/v3";
import * as z4 from "zod/v4";
import Ajv from "ajv";
Expand Down Expand Up @@ -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)}`,
);
Expand Down
9 changes: 5 additions & 4 deletions js/src/framework2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { debugLogger } from "./debug-logger";
import { _initializeSpanContext } from "./framework";
import type { Trace } from "./trace";
import iso from "./isomorph";
Expand Down Expand Up @@ -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.",
);
}
Expand Down
Loading
Loading