From 8ccc89e07a74bcacc6082ff286ad5d40cdcce766 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 16 Jul 2026 19:43:41 +0800 Subject: [PATCH 1/2] feat: add span origin provenance --- .changeset/span-origin-provenance.md | 6 + e2e/helpers/normalize.ts | 53 +++++ .../__snapshots__/log-payloads.json | 6 +- .../vitest.runner-evals-reporter.config.mts | 7 + .../vitest.runner-v3.config.mts | 15 ++ .../vitest.runner-v4.config.mts | 15 ++ .../vitest.runner.config.mts | 15 ++ integrations/otel-js/otel-v1/vitest.config.ts | 7 + integrations/otel-js/otel-v2/vitest.config.ts | 7 + integrations/otel-js/src/otel.test.ts | 200 +++++++++++++++++- integrations/otel-js/src/otel.ts | 135 +++++++++++- integrations/otel-js/tsup.config.ts | 8 + integrations/otel-js/vitest.config.ts | 8 + js/src/logger.ts | 16 +- js/src/node/config.ts | 40 +++- js/src/span-origin.test.ts | 63 ++++++ js/src/span-origin.ts | 131 ++++++++++++ js/src/wrappers/vitest/vitest.config.js | 8 + js/tsup.config.ts | 18 ++ js/vitest.config.js | 8 + 20 files changed, 759 insertions(+), 7 deletions(-) create mode 100644 .changeset/span-origin-provenance.md create mode 100644 js/src/span-origin.test.ts create mode 100644 js/src/span-origin.ts diff --git a/.changeset/span-origin-provenance.md b/.changeset/span-origin-provenance.md new file mode 100644 index 000000000..18985515b --- /dev/null +++ b/.changeset/span-origin-provenance.md @@ -0,0 +1,6 @@ +--- +"braintrust": minor +"@braintrust/otel": minor +--- + +Add span origin provenance metadata to Braintrust and OpenTelemetry spans. diff --git a/e2e/helpers/normalize.ts b/e2e/helpers/normalize.ts index 6b10658bd..e57330656 100644 --- a/e2e/helpers/normalize.ts +++ b/e2e/helpers/normalize.ts @@ -263,6 +263,24 @@ function normalizeObject( return []; } + if (key === "span_origin") { + return []; + } + + if (key === "braintrust.context_json" && typeof entry === "string") { + const normalizedContextJson = normalizeJsonString( + entry, + tokenMaps, + options, + ); + if (normalizedContextJson === "{}") { + return []; + } + if (normalizedContextJson) { + return [[key, normalizedContextJson]]; + } + } + if (isNodeInternalCaller) { if (key === "caller_filename") { return [[key, ""]]; @@ -283,6 +301,16 @@ function normalizeObject( return [[key, tokenFor(tokenMaps.ids, entry, "signature")]]; } + if ( + key === "environment" && + entry && + typeof entry === "object" && + !Array.isArray(entry) && + "type" in entry + ) { + return []; + } + return [[key, normalizeValue(entry as Json, tokenMaps, options, key)]]; }), ); @@ -341,6 +369,17 @@ function normalizeValue( } if (typeof value === "string") { + if (currentKey === "braintrust.context_json") { + const normalizedContextJson = normalizeJsonString( + value, + tokenMaps, + options, + ); + if (normalizedContextJson) { + return normalizedContextJson; + } + } + value = normalizeStackLikeString(value); const normalizedUrl = normalizeMockServerUrl(value); @@ -461,6 +500,20 @@ function normalizeValue( return value; } +function normalizeJsonString( + value: string, + tokenMaps: TokenMaps, + options: ResolvedNormalizeOptions, +): string | undefined { + try { + return JSON.stringify( + normalizeValue(JSON.parse(value) as Json, tokenMaps, options), + ); + } catch { + return undefined; + } +} + function resolveNormalizeOptions( options: NormalizeOptions | undefined, ): ResolvedNormalizeOptions { diff --git a/e2e/scenarios/langgraph-auto-instrumentation/__snapshots__/log-payloads.json b/e2e/scenarios/langgraph-auto-instrumentation/__snapshots__/log-payloads.json index d649a4b4a..422cb001d 100644 --- a/e2e/scenarios/langgraph-auto-instrumentation/__snapshots__/log-payloads.json +++ b/e2e/scenarios/langgraph-auto-instrumentation/__snapshots__/log-payloads.json @@ -373,7 +373,11 @@ ] }, { - "context": {}, + "context": { + "caller_filename": "", + "caller_functionname": "", + "caller_lineno": 0 + }, "created": "", "id": "", "input": [ diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts index f2a1afe65..389a2d045 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; import { defineConfig } from "vitest/config"; @@ -15,8 +16,14 @@ if (!testRunId) { const { default: BraintrustVitestEvalsReporter } = await import( pathToFileURL(path.join(repoRoot, "js/dist/vitest-evals-reporter.mjs")).href ); +const packageJson = JSON.parse( + readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), +); export default defineConfig({ + define: { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), + }, test: { hookTimeout: 30_000, include: ["runner.vitest-evals-reporter.case.ts"], diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts index 5399a4ac2..56b90d0f7 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts @@ -1,8 +1,23 @@ +import { readFileSync } from "node:fs"; +import * as path from "node:path"; + const vitestConfigPackageName = process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v3-latest"; const { defineConfig } = await import(`${vitestConfigPackageName}/config`); +const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; +if (!repoRoot) { + throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); +} + +const packageJson = JSON.parse( + readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), +); + export default defineConfig({ + define: { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), + }, test: { hookTimeout: 30_000, include: ["runner.case.ts"], diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts index fb9e5e4e0..07cedde80 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts @@ -1,8 +1,23 @@ +import { readFileSync } from "node:fs"; +import * as path from "node:path"; + const vitestConfigPackageName = process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v4-latest"; const { defineConfig } = await import(`${vitestConfigPackageName}/config`); +const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; +if (!repoRoot) { + throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); +} + +const packageJson = JSON.parse( + readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), +); + export default defineConfig({ + define: { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), + }, test: { hookTimeout: 30_000, include: ["runner.case.ts"], diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts index bd69a02f4..88189f676 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts @@ -1,8 +1,23 @@ +import { readFileSync } from "node:fs"; +import * as path from "node:path"; + const vitestConfigPackageName = process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v2-latest"; const { defineConfig } = await import(`${vitestConfigPackageName}/config`); +const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; +if (!repoRoot) { + throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); +} + +const packageJson = JSON.parse( + readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), +); + export default defineConfig({ + define: { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), + }, test: { hookTimeout: 30_000, include: ["runner.case.ts"], diff --git a/integrations/otel-js/otel-v1/vitest.config.ts b/integrations/otel-js/otel-v1/vitest.config.ts index 0ef9b7448..6c805c533 100644 --- a/integrations/otel-js/otel-v1/vitest.config.ts +++ b/integrations/otel-js/otel-v1/vitest.config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "vitest/config"; import { detectOtelVersion, @@ -5,12 +6,18 @@ import { createOtelAliases, } from "../tests/utils"; +const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +) as { version: string }; const cwd = process.cwd(); const version = detectOtelVersion(cwd); logOtelVersions(version); export default defineConfig({ + define: { + __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), + }, resolve: version !== "parent" ? { diff --git a/integrations/otel-js/otel-v2/vitest.config.ts b/integrations/otel-js/otel-v2/vitest.config.ts index 0ef9b7448..6c805c533 100644 --- a/integrations/otel-js/otel-v2/vitest.config.ts +++ b/integrations/otel-js/otel-v2/vitest.config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "vitest/config"; import { detectOtelVersion, @@ -5,12 +6,18 @@ import { createOtelAliases, } from "../tests/utils"; +const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +) as { version: string }; const cwd = process.cwd(); const version = detectOtelVersion(cwd); logOtelVersions(version); export default defineConfig({ + define: { + __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), + }, resolve: version !== "parent" ? { diff --git a/integrations/otel-js/src/otel.test.ts b/integrations/otel-js/src/otel.test.ts index 5f8d543d9..7522450da 100644 --- a/integrations/otel-js/src/otel.test.ts +++ b/integrations/otel-js/src/otel.test.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ +import { readFileSync } from "node:fs"; import { describe, it, expect, beforeEach, afterEach, vi, test } from "vitest"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -34,13 +35,18 @@ import { import { SpanComponentsV3, SpanComponentsV4 } from "braintrust/util"; import { setupOtelCompat, resetOtelCompat } from "."; -async function withEmptyBraintrustEnvFile( +const packageJson = JSON.parse( + readFileSync(path.join(__dirname, "../package.json"), "utf8"), +) as { version: string }; + +async function withBraintrustEnvFile( + content: string, fn: () => T | Promise, ): Promise { const originalCwd = process.cwd(); const tempDir = await mkdtemp(path.join(tmpdir(), "braintrust-otel-env-")); try { - await writeFile(path.join(tempDir, ".env.braintrust"), ""); + await writeFile(path.join(tempDir, ".env.braintrust"), content); process.chdir(tempDir); return fn(); } finally { @@ -49,6 +55,12 @@ async function withEmptyBraintrustEnvFile( } } +async function withEmptyBraintrustEnvFile( + fn: () => T | Promise, +): Promise { + return withBraintrustEnvFile("", fn); +} + describe("AISpanProcessor", () => { let memoryExporter: InMemorySpanExporter; let provider: BasicTracerProvider; @@ -178,6 +190,35 @@ describe("AISpanProcessor", () => { expect(spans.find((s) => s.attributes["foo"] === "bar")).toBeUndefined(); }); + it("should ignore Braintrust system attributes when filtering AI spans", async () => { + const rootSpan = tracer.startSpan("root"); + + const parentContext = trace.setSpanContext( + context.active(), + rootSpan.spanContext(), + ); + const systemAttrSpan = tracer.startSpan( + "some.operation", + { + attributes: { + "braintrust.context_json": JSON.stringify({ + span_origin: { name: "braintrust.sdk.javascript" }, + }), + "braintrust.parent": "project_name:test", + }, + }, + parentContext, + ); + + systemAttrSpan.end(); + rootSpan.end(); + + await provider.forceFlush(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans.find((s) => s.name === "some.operation")).toBeUndefined(); + }); + it("should support custom filter that keeps root spans using isRootSpan", () => { const customFilter = (span: ReadableSpan) => { return isRootSpan(span) ? true : undefined; @@ -754,6 +795,161 @@ describe("BraintrustSpanProcessor", () => { await expect(processor.forceFlush()).resolves.toBeUndefined(); }); + it("should merge span origin with context_json set after span start", () => { + let endedSpan: ReadableSpan | undefined; + const innerProcessor = { + onStart: vi.fn(), + onEnd: vi.fn((span: ReadableSpan) => { + endedSpan = span; + }), + shutdown: vi.fn(async () => undefined), + forceFlush: vi.fn(async () => undefined), + }; + + const processor = new BraintrustSpanProcessor({ + apiKey: "test-api-key", + _spanProcessor: innerProcessor as any, + }); + + const mockSpan = { + spanContext: () => ({ traceId: "test-trace", spanId: "test-span" }), + setAttributes: vi.fn(), + name: "late-context", + attributes: { + "braintrust.context_json": JSON.stringify({ + metadata: { source: "late-attribute" }, + }), + }, + parentSpanContext: undefined, + } as any; + + processor.onStart(mockSpan, context.active()); + processor.onEnd(mockSpan); + + expect(endedSpan).toBeDefined(); + const contextJson = endedSpan?.attributes["braintrust.context_json"]; + expect(typeof contextJson).toBe("string"); + const parsed = JSON.parse(contextJson as string); + expect(parsed.metadata.source).toBe("late-attribute"); + expect(parsed.span_origin.name).toBe("braintrust.sdk.javascript"); + expect(parsed.span_origin.version).toBe(packageJson.version); + expect(parsed.span_origin.instrumentation.name).toBe("braintrust-otel-js"); + }); + + it("should preserve explicit environment name without type", () => { + process.env.BRAINTRUST_ENVIRONMENT_NAME = "staging"; + delete process.env.BRAINTRUST_ENVIRONMENT_TYPE; + + let endedSpan: ReadableSpan | undefined; + const innerProcessor = { + onStart: vi.fn(), + onEnd: vi.fn((span: ReadableSpan) => { + endedSpan = span; + }), + shutdown: vi.fn(async () => undefined), + forceFlush: vi.fn(async () => undefined), + }; + + const processor = new BraintrustSpanProcessor({ + apiKey: "test-api-key", + _spanProcessor: innerProcessor as any, + }); + + const mockSpan = { + spanContext: () => ({ traceId: "test-trace", spanId: "test-span" }), + setAttributes: vi.fn(), + name: "environment-name-only", + attributes: {}, + parentSpanContext: undefined, + } as any; + + processor.onStart(mockSpan, context.active()); + processor.onEnd(mockSpan); + + const parsed = JSON.parse( + endedSpan?.attributes["braintrust.context_json"] as string, + ); + expect(parsed.span_origin.environment).toEqual({ name: "staging" }); + }); + + it("should read explicit environment name from .env.braintrust", async () => { + delete process.env.BRAINTRUST_ENVIRONMENT_NAME; + delete process.env.BRAINTRUST_ENVIRONMENT_TYPE; + + await withBraintrustEnvFile( + "BRAINTRUST_ENVIRONMENT_NAME=staging\n", + async () => { + let endedSpan: ReadableSpan | undefined; + const innerProcessor = { + onStart: vi.fn(), + onEnd: vi.fn((span: ReadableSpan) => { + endedSpan = span; + }), + shutdown: vi.fn(async () => undefined), + forceFlush: vi.fn(async () => undefined), + }; + + const processor = new BraintrustSpanProcessor({ + apiKey: "test-api-key", + _spanProcessor: innerProcessor as any, + }); + + const mockSpan = { + spanContext: () => ({ traceId: "test-trace", spanId: "test-span" }), + setAttributes: vi.fn(), + name: "environment-from-file", + attributes: {}, + parentSpanContext: undefined, + } as any; + + processor.onStart(mockSpan, context.active()); + processor.onEnd(mockSpan); + + const parsed = JSON.parse( + endedSpan?.attributes["braintrust.context_json"] as string, + ); + expect(parsed.span_origin.environment).toEqual({ name: "staging" }); + }, + ); + }); + + it("should preserve custom NODE_ENV values as environment names", () => { + process.env.NODE_ENV = "preview"; + delete process.env.BRAINTRUST_ENVIRONMENT_NAME; + delete process.env.BRAINTRUST_ENVIRONMENT_TYPE; + + let endedSpan: ReadableSpan | undefined; + const innerProcessor = { + onStart: vi.fn(), + onEnd: vi.fn((span: ReadableSpan) => { + endedSpan = span; + }), + shutdown: vi.fn(async () => undefined), + forceFlush: vi.fn(async () => undefined), + }; + + const processor = new BraintrustSpanProcessor({ + apiKey: "test-api-key", + _spanProcessor: innerProcessor as any, + }); + + const mockSpan = { + spanContext: () => ({ traceId: "test-trace", spanId: "test-span" }), + setAttributes: vi.fn(), + name: "custom-node-env", + attributes: {}, + parentSpanContext: undefined, + } as any; + + processor.onStart(mockSpan, context.active()); + processor.onEnd(mockSpan); + + const parsed = JSON.parse( + endedSpan?.attributes["braintrust.context_json"] as string, + ); + expect(parsed.span_origin.environment).toEqual({ name: "preview" }); + }); + it("should use default parent when none is provided", () => { process.env.BRAINTRUST_API_KEY = "test-api-key"; delete process.env.BRAINTRUST_PARENT; diff --git a/integrations/otel-js/src/otel.ts b/integrations/otel-js/src/otel.ts index 3a9d0f874..dc7e23aca 100644 --- a/integrations/otel-js/src/otel.ts +++ b/integrations/otel-js/src/otel.ts @@ -25,6 +25,8 @@ import { type Span as BraintrustSpan, } from "braintrust"; +declare const __BRAINTRUST_OTEL_VERSION__: string; + interface ExportResult { code: number; error?: Error; @@ -38,6 +40,11 @@ const FILTER_PREFIXES = [ "traceloop.", ] as const; +const SYSTEM_ATTRIBUTE_NAMES = new Set([ + "braintrust.parent", + "braintrust.context_json", +]); + /** * Custom filter function type for span filtering. * @param span - The span to evaluate @@ -72,7 +79,9 @@ function isAISpan(span: ReadableSpan): boolean { } const attributes = span.attributes; if (attributes) { - const attributeNames = Object.keys(attributes); + const attributeNames = Object.keys(attributes).filter( + (name) => !SYSTEM_ATTRIBUTE_NAMES.has(name), + ); if ( attributeNames.some((name) => FILTER_PREFIXES.some((prefix) => name.startsWith(prefix)), @@ -192,6 +201,7 @@ interface BraintrustSpanProcessorOptions { * Additional headers to send with telemetry data */ headers?: Record; + environment?: { type?: string; name?: string }; /** * @internal * Internal option for dependency injection during testing. @@ -200,6 +210,123 @@ interface BraintrustSpanProcessorOptions { _spanProcessor?: SpanProcessor; } +const SDK_VERSION = __BRAINTRUST_OTEL_VERSION__; + +function spanOriginContext(environment?: { type?: string; name?: string }) { + return { + span_origin: { + name: "braintrust.sdk.javascript", + version: SDK_VERSION, + instrumentation: { name: "braintrust-otel-js" }, + ...(environment ? { environment } : {}), + }, + }; +} + +function mergeSpanOriginContextJson( + existingContextJson: unknown, + environment?: { type?: string; name?: string }, +): string { + const context = + typeof existingContextJson === "string" && existingContextJson + ? parseContextJson(existingContextJson) + : {}; + const existingOrigin = + context.span_origin && + typeof context.span_origin === "object" && + !Array.isArray(context.span_origin) + ? context.span_origin + : {}; + return JSON.stringify({ + ...context, + span_origin: { + ...spanOriginContext(environment).span_origin, + ...existingOrigin, + }, + }); +} + +function parseContextJson(contextJson: string): Record { + try { + const parsed = JSON.parse(contextJson); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? parsed + : {}; + } catch { + return {}; + } +} + +function withSpanOriginAttributes( + span: ReadableSpan, + environment?: { type?: string; name?: string }, +): ReadableSpan { + const attributes = { + ...span.attributes, + "braintrust.context_json": mergeSpanOriginContextJson( + span.attributes["braintrust.context_json"], + environment, + ), + }; + return new Proxy(span, { + get(target, prop, receiver) { + if (prop === "attributes") return attributes; + const value = Reflect.get(target, prop, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + +function detectEnvironment(explicit?: { + type?: string; + name?: string; +}): { type?: string; name?: string } | undefined { + if (explicit) return explicit; + const envType = _internalIso.getEnv("BRAINTRUST_ENVIRONMENT_TYPE"); + const envName = _internalIso.getEnv("BRAINTRUST_ENVIRONMENT_NAME"); + if (envType || envName) { + return { + ...(envType ? { type: envType } : {}), + ...(envName ? { name: envName } : {}), + }; + } + if (_internalIso.getEnv("GITHUB_ACTIONS")) + return { type: "ci", name: "github_actions" }; + if (_internalIso.getEnv("GITLAB_CI")) + return { type: "ci", name: "gitlab_ci" }; + if (_internalIso.getEnv("CIRCLECI")) return { type: "ci", name: "circleci" }; + if (_internalIso.getEnv("BUILDKITE")) + return { type: "ci", name: "buildkite" }; + if (_internalIso.getEnv("CI")) return { type: "ci", name: "ci" }; + if (_internalIso.getEnv("VERCEL")) return { type: "server", name: "vercel" }; + if (_internalIso.getEnv("NETLIFY")) + return { type: "server", name: "netlify" }; + const awsExecutionEnv = _internalIso.getEnv("AWS_EXECUTION_ENV"); + if ( + _internalIso.getEnv("ECS_CONTAINER_METADATA_URI") || + _internalIso.getEnv("ECS_CONTAINER_METADATA_URI_V4") || + awsExecutionEnv?.startsWith("AWS_ECS_") + ) { + return { type: "server", name: "ecs" }; + } + if ( + _internalIso.getEnv("AWS_LAMBDA_FUNCTION_NAME") || + awsExecutionEnv?.startsWith("AWS_Lambda_") + ) { + return { type: "server", name: "aws_lambda" }; + } + const nodeEnv = _internalIso.getEnv("NODE_ENV"); + if (!nodeEnv) return undefined; + const normalizedNodeEnv = nodeEnv.toLowerCase(); + if (normalizedNodeEnv === "production" || normalizedNodeEnv === "staging") { + return { type: "server", name: normalizedNodeEnv }; + } + if (normalizedNodeEnv === "development" || normalizedNodeEnv === "local") { + return { type: "local", name: normalizedNodeEnv }; + } + return { name: nodeEnv }; +} + class LazyBraintrustOTLPTraceExporter implements SpanExporter { private readonly diagLogger = diag.createComponentLogger({ namespace: "@braintrust/otel", @@ -377,8 +504,10 @@ export class BraintrustSpanProcessor implements SpanProcessor { private readonly processor: SpanProcessor; private readonly aiSpanProcessor: SpanProcessor; private readonly exporter?: LazyBraintrustOTLPTraceExporter; + private readonly environment?: { type?: string; name?: string }; constructor(options: BraintrustSpanProcessorOptions = {}) { + this.environment = detectEnvironment(options.environment); // If a processor is injected (for testing), use it directly if (options._spanProcessor) { this.processor = options._spanProcessor; @@ -524,7 +653,9 @@ export class BraintrustSpanProcessor implements SpanProcessor { } onEnd(span: ReadableSpan): void { - this.aiSpanProcessor.onEnd(span); + this.aiSpanProcessor.onEnd( + withSpanOriginAttributes(span, this.environment), + ); } shutdown(): Promise { diff --git a/integrations/otel-js/tsup.config.ts b/integrations/otel-js/tsup.config.ts index 08eab2329..05319102e 100644 --- a/integrations/otel-js/tsup.config.ts +++ b/integrations/otel-js/tsup.config.ts @@ -1,4 +1,9 @@ import { defineConfig } from "tsup"; +import { readFileSync } from "node:fs"; + +const packageJson = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) as { version: string }; export default defineConfig([ { @@ -13,6 +18,9 @@ export default defineConfig([ "@opentelemetry/sdk-trace-base", "@opentelemetry/context-async-hooks", ], + define: { + __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), + }, dts: true, }, ]); diff --git a/integrations/otel-js/vitest.config.ts b/integrations/otel-js/vitest.config.ts index 744610c82..5da49856a 100644 --- a/integrations/otel-js/vitest.config.ts +++ b/integrations/otel-js/vitest.config.ts @@ -1,6 +1,14 @@ import { defineConfig } from "vitest/config"; +import { readFileSync } from "node:fs"; + +const packageJson = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) as { version: string }; export default defineConfig({ + define: { + __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), + }, test: { reporters: ["default"], include: ["tests/**/*.test.ts", "src/**/*.test.ts"], diff --git a/js/src/logger.ts b/js/src/logger.ts index ab56bed90..905512568 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -180,6 +180,11 @@ import { lintTemplate as _lintMustacheTemplate } from "./template/mustache-utils import { prettifyXact } from "../util/index"; import { SpanCache, CachedSpan } from "./span-cache"; import type { EvalParameters, InferParameters } from "./eval-parameters"; +import { + detectSpanOriginEnvironment, + mergeSpanOriginContext, + type SpanOriginEnvironment, +} from "./span-origin"; // Manual type definition for inline attachments (not in generated_types) const InlineAttachmentReferenceSchema = z.object({ @@ -752,6 +757,7 @@ export class BraintrustState { private _idGenerator: IDGenerator | null = null; private _contextManager: ContextManager | null = null; private _otelFlushCallback: (() => Promise) | null = null; + public spanOriginEnvironment: SpanOriginEnvironment | undefined; constructor(private loginParams: LoginOptions) { this.id = `${new Date().toLocaleString()}-${stateNonce++}`; // This is for debugging. uuidv4() breaks on platforms like Cloudflare. @@ -813,6 +819,7 @@ export class BraintrustState { }); this.spanCache = new SpanCache({ disabled: loginParams.disableSpanCache }); + this.spanOriginEnvironment = detectSpanOriginEnvironment(); } public resetLoginInfo() { @@ -4610,6 +4617,7 @@ type AsyncFlushArg = { export type InitLoggerOptions = FullLoginOptions & { projectName?: string; projectId?: string; + environment?: SpanOriginEnvironment; setCurrent?: boolean; state?: BraintrustState; orgProjectMetadata?: OrgProjectMetadata; @@ -4643,6 +4651,7 @@ export function initLogger( orgName, forceLogin, debugLogLevel, + environment, fetch, state: stateArg, } = options || {}; @@ -4664,6 +4673,7 @@ export function initLogger( const state = stateArg ?? _globalState; state.setDebugLogLevel(debugLogLevel); + state.spanOriginEnvironment = detectSpanOriginEnvironment(environment); // Enable queue size limit enforcement for initLogger() calls // This ensures production observability doesn't OOM customer processes @@ -7620,7 +7630,11 @@ export class SpanImpl implements Span { metrics: { start: args.startTime ?? getCurrentUnixTimestamp(), }, - context: { ...callerLocation }, + context: mergeSpanOriginContext( + { ...callerLocation }, + "braintrust-js-logger", + this._state.spanOriginEnvironment, + ), span_attributes: { name, type, diff --git a/js/src/node/config.ts b/js/src/node/config.ts index 09c27b807..b7c8d98b6 100644 --- a/js/src/node/config.ts +++ b/js/src/node/config.ts @@ -31,7 +31,17 @@ export function configureNode() { iso.getPastNAncestors = getPastNAncestors; iso.getEnv = (name) => { const value = process.env[name]; - return name === "BRAINTRUST_API_KEY" && !value?.trim() ? undefined : value; + if (name === "BRAINTRUST_API_KEY") { + return value?.trim() ? value : undefined; + } + if ( + (name === "BRAINTRUST_ENVIRONMENT_TYPE" || + name === "BRAINTRUST_ENVIRONMENT_NAME") && + !value?.trim() + ) { + return getNearestBraintrustEnvValue(name); + } + return value; }; iso.getBraintrustApiKey = async () => { const value = process.env.BRAINTRUST_API_KEY; @@ -152,3 +162,31 @@ export function configureNode() { // Enable auto-instrumentation registry.enable(); } + +function getNearestBraintrustEnvValue(name: string): string | undefined { + for ( + let dir = process.cwd(), depth = 0; + depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; + dir = path.dirname(dir), depth++ + ) { + const envPath = path.join(dir, ".env.braintrust"); + try { + const parsed = dotenv.parse(fsSync.readFileSync(envPath, "utf8")); + const value = parsed[name]; + return value?.trim() ? value : undefined; + } catch (e) { + if ( + typeof e !== "object" || + e === null || + !("code" in e) || + e.code !== "ENOENT" + ) { + return undefined; + } + } + if (path.dirname(dir) === dir) { + break; + } + } + return undefined; +} diff --git a/js/src/span-origin.test.ts b/js/src/span-origin.test.ts new file mode 100644 index 000000000..a551722c5 --- /dev/null +++ b/js/src/span-origin.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from "node:fs"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import iso from "./isomorph"; +import { + detectSpanOriginEnvironment, + mergeSpanOriginContext, +} from "./span-origin"; + +const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +) as { version: string }; + +const originalGetEnv = iso.getEnv; + +afterEach(() => { + iso.getEnv = originalGetEnv; + vi.restoreAllMocks(); +}); + +describe("mergeSpanOriginContext", () => { + it("loads the SDK version from package.json", () => { + const context = mergeSpanOriginContext(undefined, "test-instrumentation"); + + expect(context.span_origin).toMatchObject({ + name: "braintrust.sdk.javascript", + version: packageJson.version, + instrumentation: { name: "test-instrumentation" }, + }); + }); +}); + +describe("detectSpanOriginEnvironment", () => { + it("uses production NODE_ENV as a server environment", () => { + vi.spyOn(iso, "getEnv").mockImplementation((name) => + name === "NODE_ENV" ? "production" : undefined, + ); + + expect(detectSpanOriginEnvironment()).toEqual({ + type: "server", + name: "production", + }); + }); + + it("uses development NODE_ENV as a local environment", () => { + vi.spyOn(iso, "getEnv").mockImplementation((name) => + name === "NODE_ENV" ? "development" : undefined, + ); + + expect(detectSpanOriginEnvironment()).toEqual({ + type: "local", + name: "development", + }); + }); + + it("preserves custom NODE_ENV values as environment names", () => { + vi.spyOn(iso, "getEnv").mockImplementation((name) => + name === "NODE_ENV" ? "preview" : undefined, + ); + + expect(detectSpanOriginEnvironment()).toEqual({ name: "preview" }); + }); +}); diff --git a/js/src/span-origin.ts b/js/src/span-origin.ts new file mode 100644 index 000000000..f89577b0c --- /dev/null +++ b/js/src/span-origin.ts @@ -0,0 +1,131 @@ +import iso from "./isomorph"; + +declare const __BRAINTRUST_SDK_VERSION__: string; + +export type SpanOriginEnvironment = { + type?: string; + name?: string; +}; + +type SpanOrigin = { + name: string; + version: string; + instrumentation: { name: string }; + environment?: SpanOriginEnvironment; +}; + +const SDK_VERSION = __BRAINTRUST_SDK_VERSION__; + +export function detectSpanOriginEnvironment( + explicit?: SpanOriginEnvironment, +): SpanOriginEnvironment | undefined { + if (explicit) return explicit; + + const envType = iso.getEnv("BRAINTRUST_ENVIRONMENT_TYPE"); + const envName = iso.getEnv("BRAINTRUST_ENVIRONMENT_NAME"); + if (envType || envName) { + return { + ...(envType ? { type: envType } : {}), + ...(envName ? { name: envName } : {}), + }; + } + + const ci = firstPresent([ + ["GITHUB_ACTIONS", "github_actions"], + ["GITLAB_CI", "gitlab_ci"], + ["CIRCLECI", "circleci"], + ["BUILDKITE", "buildkite"], + ["JENKINS_URL", "jenkins"], + ["JENKINS_HOME", "jenkins"], + ["TF_BUILD", "azure_pipelines"], + ["TEAMCITY_VERSION", "teamcity"], + ["TRAVIS", "travis"], + ["BITBUCKET_BUILD_NUMBER", "bitbucket"], + ]); + if (ci) return { type: "ci", name: ci }; + if (iso.getEnv("CI")) return { type: "ci", name: "ci" }; + + const earlyServer = firstPresent([ + ["VERCEL", "vercel"], + ["NETLIFY", "netlify"], + ]); + if (earlyServer) return { type: "server", name: earlyServer }; + if ( + iso.getEnv("ECS_CONTAINER_METADATA_URI") || + iso.getEnv("ECS_CONTAINER_METADATA_URI_V4") + ) { + return { type: "server", name: "ecs" }; + } + const awsExecutionEnv = iso.getEnv("AWS_EXECUTION_ENV"); + if (awsExecutionEnv?.startsWith("AWS_ECS_")) { + return { type: "server", name: "ecs" }; + } + if (awsExecutionEnv?.startsWith("AWS_Lambda_")) { + return { type: "server", name: "aws_lambda" }; + } + if (iso.getEnv("AWS_LAMBDA_FUNCTION_NAME")) { + return { type: "server", name: "aws_lambda" }; + } + + const server = firstPresent([ + ["K_SERVICE", "cloud_run"], + ["FUNCTION_TARGET", "gcp_functions"], + ["KUBERNETES_SERVICE_HOST", "kubernetes"], + ["DYNO", "heroku"], + ["FLY_APP_NAME", "fly"], + ["RAILWAY_ENVIRONMENT", "railway"], + ["RENDER_SERVICE_NAME", "render"], + ]); + if (server) return { type: "server", name: server }; + + return deploymentModeEnvironment("NODE_ENV", iso.getEnv("NODE_ENV")); +} + +function makeSpanOrigin( + instrumentationName: string, + environment?: SpanOriginEnvironment, +): SpanOrigin { + return { + name: "braintrust.sdk.javascript", + version: SDK_VERSION, + instrumentation: { name: instrumentationName }, + ...(environment ? { environment } : {}), + }; +} + +export function mergeSpanOriginContext( + context: Record | undefined, + instrumentationName: string, + environment?: SpanOriginEnvironment, +): Record { + const next = { ...(context ?? {}) }; + const current = isObject(next.span_origin) ? { ...next.span_origin } : {}; + next.span_origin = { + ...makeSpanOrigin(instrumentationName, environment), + ...current, + }; + return next; +} + +function firstPresent(entries: Array<[string, string]>): string | undefined { + return entries.find(([key]) => Boolean(iso.getEnv(key)))?.[1]; +} + +function deploymentModeEnvironment( + _key: string, + value: string | undefined, +): SpanOriginEnvironment | undefined { + if (!value) return undefined; + const normalized = value.toLowerCase(); + if (normalized === "production" || normalized === "staging") { + return { type: "server", name: normalized }; + } + if (normalized === "development" || normalized === "local") { + return { type: "local", name: normalized }; + } + return { name: value }; +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/js/src/wrappers/vitest/vitest.config.js b/js/src/wrappers/vitest/vitest.config.js index c6b85f3bf..cb26fae2c 100644 --- a/js/src/wrappers/vitest/vitest.config.js +++ b/js/src/wrappers/vitest/vitest.config.js @@ -1,6 +1,14 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "vitest/config"; +const packageJson = JSON.parse( + readFileSync(new URL("../../../package.json", import.meta.url), "utf8"), +); + export default defineConfig({ + define: { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), + }, test: { hookTimeout: 60_000, reporters: ["default"], diff --git a/js/tsup.config.ts b/js/tsup.config.ts index a56751745..9780ad447 100644 --- a/js/tsup.config.ts +++ b/js/tsup.config.ts @@ -1,4 +1,13 @@ import { defineConfig } from "tsup"; +import { readFileSync } from "node:fs"; + +const packageJson = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) as { version: string }; + +const define = { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), +}; export default defineConfig([ // Node.js entrypoint @@ -20,6 +29,7 @@ export default defineConfig([ }, }, splitting: true, + define, clean: true, }, { @@ -30,6 +40,7 @@ export default defineConfig([ external: ["esbuild", "prettier", "typescript", "zod"], // CLI doesn't need DTS dts: false, + define, clean: false, }, { @@ -45,6 +56,7 @@ export default defineConfig([ }, }, splitting: true, + define, clean: true, }, { @@ -60,6 +72,7 @@ export default defineConfig([ }, }, splitting: true, + define, clean: true, }, // Browser/edge entrypoints @@ -78,6 +91,7 @@ export default defineConfig([ platform: "browser", splitting: false, dts: true, + define, clean: false, }, { @@ -91,6 +105,7 @@ export default defineConfig([ }, }, splitting: false, + define, clean: true, }, { @@ -108,6 +123,7 @@ export default defineConfig([ outDir: "dist/auto-instrumentations", dts: true, external: ["zod"], + define, outExtension({ format }) { if (format === "esm") { return { js: ".mjs" }; @@ -125,6 +141,7 @@ export default defineConfig([ outDir: "dist/auto-instrumentations", dts: true, external: ["zod"], + define, outExtension() { return { js: ".cjs" }; }, @@ -140,6 +157,7 @@ export default defineConfig([ dts: false, platform: "node", external: ["@anthropic-ai/sdk", "zod"], + define, outExtension({ format }) { return { js: ".mjs" }; }, diff --git a/js/vitest.config.js b/js/vitest.config.js index 7d680a0a5..4bade6d99 100644 --- a/js/vitest.config.js +++ b/js/vitest.config.js @@ -1,7 +1,15 @@ import tsconfigPaths from "vite-tsconfig-paths"; import path from "path"; +import { readFileSync } from "node:fs"; + +const packageJson = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +); const config = { + define: { + __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), + }, plugins: [ tsconfigPaths({ // Explicitly specify the root tsconfig to prevent scanning vendor folders From 98856c7ba89eaa8161f86c3cc531714fa3d91ba3 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 17 Jul 2026 19:21:30 +0800 Subject: [PATCH 2/2] fix: simplify SDK version define for span origin --- .../vitest.runner-evals-reporter.config.mts | 7 ------- .../vitest.runner-v3.config.mts | 15 --------------- .../vitest.runner-v4.config.mts | 15 --------------- .../vitest.runner.config.mts | 15 --------------- js/src/span-origin.test.ts | 9 ++------- js/src/span-origin.ts | 5 ++++- js/src/wrappers/vitest/vitest.config.js | 8 -------- js/vitest.config.js | 8 -------- 8 files changed, 6 insertions(+), 76 deletions(-) diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts index 389a2d045..f2a1afe65 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts @@ -1,4 +1,3 @@ -import { readFileSync } from "node:fs"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; import { defineConfig } from "vitest/config"; @@ -16,14 +15,8 @@ if (!testRunId) { const { default: BraintrustVitestEvalsReporter } = await import( pathToFileURL(path.join(repoRoot, "js/dist/vitest-evals-reporter.mjs")).href ); -const packageJson = JSON.parse( - readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), -); export default defineConfig({ - define: { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), - }, test: { hookTimeout: 30_000, include: ["runner.vitest-evals-reporter.case.ts"], diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts index 56b90d0f7..5399a4ac2 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v3.config.mts @@ -1,23 +1,8 @@ -import { readFileSync } from "node:fs"; -import * as path from "node:path"; - const vitestConfigPackageName = process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v3-latest"; const { defineConfig } = await import(`${vitestConfigPackageName}/config`); -const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; -if (!repoRoot) { - throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); -} - -const packageJson = JSON.parse( - readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), -); - export default defineConfig({ - define: { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), - }, test: { hookTimeout: 30_000, include: ["runner.case.ts"], diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts index 07cedde80..fb9e5e4e0 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v4.config.mts @@ -1,23 +1,8 @@ -import { readFileSync } from "node:fs"; -import * as path from "node:path"; - const vitestConfigPackageName = process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v4-latest"; const { defineConfig } = await import(`${vitestConfigPackageName}/config`); -const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; -if (!repoRoot) { - throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); -} - -const packageJson = JSON.parse( - readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), -); - export default defineConfig({ - define: { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), - }, test: { hookTimeout: 30_000, include: ["runner.case.ts"], diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts index 88189f676..bd69a02f4 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner.config.mts @@ -1,23 +1,8 @@ -import { readFileSync } from "node:fs"; -import * as path from "node:path"; - const vitestConfigPackageName = process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v2-latest"; const { defineConfig } = await import(`${vitestConfigPackageName}/config`); -const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; -if (!repoRoot) { - throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); -} - -const packageJson = JSON.parse( - readFileSync(path.join(repoRoot, "js/package.json"), "utf8"), -); - export default defineConfig({ - define: { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), - }, test: { hookTimeout: 30_000, include: ["runner.case.ts"], diff --git a/js/src/span-origin.test.ts b/js/src/span-origin.test.ts index a551722c5..d526b2343 100644 --- a/js/src/span-origin.test.ts +++ b/js/src/span-origin.test.ts @@ -1,4 +1,3 @@ -import { readFileSync } from "node:fs"; import { afterEach, describe, expect, it, vi } from "vitest"; import iso from "./isomorph"; @@ -7,10 +6,6 @@ import { mergeSpanOriginContext, } from "./span-origin"; -const packageJson = JSON.parse( - readFileSync(new URL("../package.json", import.meta.url), "utf8"), -) as { version: string }; - const originalGetEnv = iso.getEnv; afterEach(() => { @@ -19,12 +14,12 @@ afterEach(() => { }); describe("mergeSpanOriginContext", () => { - it("loads the SDK version from package.json", () => { + it("uses the test fallback SDK version when no build-time version is defined", () => { const context = mergeSpanOriginContext(undefined, "test-instrumentation"); expect(context.span_origin).toMatchObject({ name: "braintrust.sdk.javascript", - version: packageJson.version, + version: "0.0.0", instrumentation: { name: "test-instrumentation" }, }); }); diff --git a/js/src/span-origin.ts b/js/src/span-origin.ts index f89577b0c..5e77b50f8 100644 --- a/js/src/span-origin.ts +++ b/js/src/span-origin.ts @@ -14,7 +14,10 @@ type SpanOrigin = { environment?: SpanOriginEnvironment; }; -const SDK_VERSION = __BRAINTRUST_SDK_VERSION__; +const SDK_VERSION = + typeof __BRAINTRUST_SDK_VERSION__ !== "undefined" + ? __BRAINTRUST_SDK_VERSION__ + : "0.0.0"; export function detectSpanOriginEnvironment( explicit?: SpanOriginEnvironment, diff --git a/js/src/wrappers/vitest/vitest.config.js b/js/src/wrappers/vitest/vitest.config.js index cb26fae2c..c6b85f3bf 100644 --- a/js/src/wrappers/vitest/vitest.config.js +++ b/js/src/wrappers/vitest/vitest.config.js @@ -1,14 +1,6 @@ -import { readFileSync } from "node:fs"; import { defineConfig } from "vitest/config"; -const packageJson = JSON.parse( - readFileSync(new URL("../../../package.json", import.meta.url), "utf8"), -); - export default defineConfig({ - define: { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), - }, test: { hookTimeout: 60_000, reporters: ["default"], diff --git a/js/vitest.config.js b/js/vitest.config.js index 4bade6d99..7d680a0a5 100644 --- a/js/vitest.config.js +++ b/js/vitest.config.js @@ -1,15 +1,7 @@ import tsconfigPaths from "vite-tsconfig-paths"; import path from "path"; -import { readFileSync } from "node:fs"; - -const packageJson = JSON.parse( - readFileSync(new URL("./package.json", import.meta.url), "utf8"), -); const config = { - define: { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), - }, plugins: [ tsconfigPaths({ // Explicitly specify the root tsconfig to prevent scanning vendor folders