From 9cdadf1512f1aa9d876428682ccedd6ebedadaf0 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Mon, 13 Jul 2026 23:28:39 +0800 Subject: [PATCH 1/6] Add span origin provenance --- src/client.ts | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index afc8008..db9d060 100644 --- a/src/client.ts +++ b/src/client.ts @@ -60,6 +60,7 @@ export interface SpanData { caller_functionname?: string caller_filename?: string caller_lineno?: number + span_origin?: Record } span_attributes?: { name?: string @@ -68,6 +69,46 @@ export interface SpanData { _is_merge?: boolean // When true, merge with existing span by id instead of creating new row } +const PLUGIN_VERSION = "0.0.9" + +function detectEnvironment(): { type: string; name?: string } | undefined { + if (process.env.BRAINTRUST_ENVIRONMENT_TYPE) { + return process.env.BRAINTRUST_ENVIRONMENT_NAME + ? { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE, name: process.env.BRAINTRUST_ENVIRONMENT_NAME } + : { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE } + } + if (process.env.GITHUB_ACTIONS) return { type: "ci", name: "github_actions" } + if (process.env.GITLAB_CI) return { type: "ci", name: "gitlab_ci" } + if (process.env.CIRCLECI) return { type: "ci", name: "circleci" } + if (process.env.BUILDKITE) return { type: "ci", name: "buildkite" } + if (process.env.CI) return { type: "ci", name: "ci" } + if (process.env.VERCEL) return { type: "server", name: "vercel" } + if (process.env.NETLIFY) return { type: "server", name: "netlify" } + if (process.env.NODE_ENV === "production" || process.env.NODE_ENV === "staging") { + return { type: "server", name: process.env.NODE_ENV } + } + if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "local") { + return { type: "local", name: process.env.NODE_ENV } + } + return undefined +} + +function withSpanOrigin(span: SpanData): SpanData { + const environment = detectEnvironment() + return { + ...span, + context: { + ...(span.context ?? {}), + span_origin: { + name: "braintrust.plugin.opencode", + version: PLUGIN_VERSION, + instrumentation: { name: "opencode-tracing" }, + ...(environment ? { environment } : {}), + }, + }, + } +} + interface LoginResponse { org_info: Array<{ name: string @@ -345,7 +386,8 @@ export class BraintrustClient { } try { - const payload = { events: [span] } + const enrichedSpan = withSpanOrigin(span) + const payload = { events: [enrichedSpan] } debugLog?.("insertSpan: sending", { spanId: span.span_id, isMerge: span._is_merge, From fc0306d0339dd18b6e6b010140fb3df13bcc3ad5 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 14:48:25 +0800 Subject: [PATCH 2/6] Format environment provenance helper --- src/client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index db9d060..1cf464b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -74,7 +74,10 @@ const PLUGIN_VERSION = "0.0.9" function detectEnvironment(): { type: string; name?: string } | undefined { if (process.env.BRAINTRUST_ENVIRONMENT_TYPE) { return process.env.BRAINTRUST_ENVIRONMENT_NAME - ? { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE, name: process.env.BRAINTRUST_ENVIRONMENT_NAME } + ? { + type: process.env.BRAINTRUST_ENVIRONMENT_TYPE, + name: process.env.BRAINTRUST_ENVIRONMENT_NAME, + } : { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE } } if (process.env.GITHUB_ACTIONS) return { type: "ci", name: "github_actions" } From b10cab1432abda287c01ad6080a67eb4c49a444e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 15:13:36 +0800 Subject: [PATCH 3/6] Load OpenCode plugin version from package metadata --- src/client.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- src/client.ts | 15 ++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/client.test.ts b/src/client.test.ts index 1c3c819..1c1e5ec 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -3,7 +3,8 @@ */ import { afterEach, beforeEach, describe, expect, it } from "bun:test" -import { loadConfig, type PluginConfig, parseBooleanEnv } from "./client" +import { readFileSync } from "node:fs" +import { BraintrustClient, loadConfig, type PluginConfig, parseBooleanEnv } from "./client" describe("parseBooleanEnv", () => { it("returns false for undefined", () => { @@ -294,3 +295,45 @@ describe("loadConfig", () => { }) }) }) + +describe("BraintrustClient span origin", () => { + const originalFetch = globalThis.fetch + + afterEach(() => { + globalThis.fetch = originalFetch + }) + + it("uses the package.json version for span origin provenance", async () => { + const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), + ) as { version: string } + let payload: { events?: Array<{ context?: { span_origin?: { version?: string } } }> } = {} + globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => { + payload = JSON.parse(String(init?.body)) + return new Response(JSON.stringify({ row_ids: ["row-id"] }), { status: 200 }) + }) as typeof fetch + + const client = new BraintrustClient({ + apiKey: "key", + apiUrl: "https://api.example.com", + appUrl: "https://app.example.com", + projectName: "project", + tracingEnabled: true, + enableTools: true, + debug: false, + }) + Object.assign(client, { + initPromise: Promise.resolve(), + projectId: "project-id", + resolvedApiUrl: "https://api.example.com", + }) + + await client.insertSpan({ + id: "span-id", + span_id: "span-id", + root_span_id: "span-id", + }) + + expect(payload.events?.[0]?.context?.span_origin?.version).toBe(packageJson.version) + }) +}) diff --git a/src/client.ts b/src/client.ts index 1cf464b..7e2c15e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,6 +2,8 @@ * Braintrust API client for the OpenCode plugin */ +import { readFileSync } from "node:fs" + export interface BraintrustConfig { apiKey: string apiUrl?: string @@ -69,7 +71,18 @@ export interface SpanData { _is_merge?: boolean // When true, merge with existing span by id instead of creating new row } -const PLUGIN_VERSION = "0.0.9" +function loadPluginVersion(): string { + try { + const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), + ) as { version?: unknown } + return typeof packageJson.version === "string" ? packageJson.version : "unknown" + } catch { + return "unknown" + } +} + +const PLUGIN_VERSION = loadPluginVersion() function detectEnvironment(): { type: string; name?: string } | undefined { if (process.env.BRAINTRUST_ENVIRONMENT_TYPE) { From a623f8caa83737d9182e5e5cf0bed6bc92d11165 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 15:19:48 +0800 Subject: [PATCH 4/6] Import OpenCode plugin version metadata --- src/client.test.ts | 7 ++----- src/client.ts | 15 +-------------- src/version.ts | 5 +++++ 3 files changed, 8 insertions(+), 19 deletions(-) create mode 100644 src/version.ts diff --git a/src/client.test.ts b/src/client.test.ts index 1c1e5ec..6685d3c 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -3,7 +3,7 @@ */ import { afterEach, beforeEach, describe, expect, it } from "bun:test" -import { readFileSync } from "node:fs" +import manifest from "../package.json" with { type: "json" } import { BraintrustClient, loadConfig, type PluginConfig, parseBooleanEnv } from "./client" describe("parseBooleanEnv", () => { @@ -304,9 +304,6 @@ describe("BraintrustClient span origin", () => { }) it("uses the package.json version for span origin provenance", async () => { - const packageJson = JSON.parse( - readFileSync(new URL("../package.json", import.meta.url), "utf8"), - ) as { version: string } let payload: { events?: Array<{ context?: { span_origin?: { version?: string } } }> } = {} globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => { payload = JSON.parse(String(init?.body)) @@ -334,6 +331,6 @@ describe("BraintrustClient span origin", () => { root_span_id: "span-id", }) - expect(payload.events?.[0]?.context?.span_origin?.version).toBe(packageJson.version) + expect(payload.events?.[0]?.context?.span_origin?.version).toBe(manifest.version) }) }) diff --git a/src/client.ts b/src/client.ts index 7e2c15e..c0df8fa 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,7 +2,7 @@ * Braintrust API client for the OpenCode plugin */ -import { readFileSync } from "node:fs" +import { PLUGIN_VERSION } from "./version" export interface BraintrustConfig { apiKey: string @@ -71,19 +71,6 @@ export interface SpanData { _is_merge?: boolean // When true, merge with existing span by id instead of creating new row } -function loadPluginVersion(): string { - try { - const packageJson = JSON.parse( - readFileSync(new URL("../package.json", import.meta.url), "utf8"), - ) as { version?: unknown } - return typeof packageJson.version === "string" ? packageJson.version : "unknown" - } catch { - return "unknown" - } -} - -const PLUGIN_VERSION = loadPluginVersion() - function detectEnvironment(): { type: string; name?: string } | undefined { if (process.env.BRAINTRUST_ENVIRONMENT_TYPE) { return process.env.BRAINTRUST_ENVIRONMENT_NAME diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..b01a794 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,5 @@ +// Read the package version at build/bundle time so package.json remains the +// single source of truth for span origin provenance. +import manifest from "../package.json" with { type: "json" } + +export const PLUGIN_VERSION: string = manifest.version From 33372dd44ffd19aaaf5c6413d038b0c237ac2c7f Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 18:10:02 +0800 Subject: [PATCH 5/6] Fix span origin AWS environment detection --- src/client.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client.ts b/src/client.ts index c0df8fa..f418d3d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -87,6 +87,19 @@ function detectEnvironment(): { type: string; name?: string } | undefined { if (process.env.CI) return { type: "ci", name: "ci" } if (process.env.VERCEL) return { type: "server", name: "vercel" } if (process.env.NETLIFY) return { type: "server", name: "netlify" } + if ( + process.env.ECS_CONTAINER_METADATA_URI || + process.env.ECS_CONTAINER_METADATA_URI_V4 || + process.env.AWS_EXECUTION_ENV?.startsWith("AWS_ECS_") + ) { + return { type: "server", name: "ecs" } + } + if ( + process.env.AWS_LAMBDA_FUNCTION_NAME || + process.env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda_") + ) { + return { type: "server", name: "aws_lambda" } + } if (process.env.NODE_ENV === "production" || process.env.NODE_ENV === "staging") { return { type: "server", name: process.env.NODE_ENV } } From 414f1a8a3bd8ed7d56b0c432e157abe8a2f46240 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 22:51:11 +0800 Subject: [PATCH 6/6] fix: preserve span origin environment name --- src/client.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index f418d3d..eb440d2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -71,14 +71,16 @@ export interface SpanData { _is_merge?: boolean // When true, merge with existing span by id instead of creating new row } -function detectEnvironment(): { type: string; name?: string } | undefined { - if (process.env.BRAINTRUST_ENVIRONMENT_TYPE) { - return process.env.BRAINTRUST_ENVIRONMENT_NAME - ? { - type: process.env.BRAINTRUST_ENVIRONMENT_TYPE, - name: process.env.BRAINTRUST_ENVIRONMENT_NAME, - } - : { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE } +function detectEnvironment(): { type?: string; name?: string } | undefined { + if (process.env.BRAINTRUST_ENVIRONMENT_TYPE || process.env.BRAINTRUST_ENVIRONMENT_NAME) { + return { + ...(process.env.BRAINTRUST_ENVIRONMENT_TYPE + ? { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE } + : {}), + ...(process.env.BRAINTRUST_ENVIRONMENT_NAME + ? { name: process.env.BRAINTRUST_ENVIRONMENT_NAME } + : {}), + } } if (process.env.GITHUB_ACTIONS) return { type: "ci", name: "github_actions" } if (process.env.GITLAB_CI) return { type: "ci", name: "gitlab_ci" }