diff --git a/src/client.test.ts b/src/client.test.ts index 1c3c819..6685d3c 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 manifest from "../package.json" with { type: "json" } +import { BraintrustClient, loadConfig, type PluginConfig, parseBooleanEnv } from "./client" describe("parseBooleanEnv", () => { it("returns false for undefined", () => { @@ -294,3 +295,42 @@ 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 () => { + 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(manifest.version) + }) +}) diff --git a/src/client.ts b/src/client.ts index afc8008..eb440d2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,6 +2,8 @@ * Braintrust API client for the OpenCode plugin */ +import { PLUGIN_VERSION } from "./version" + export interface BraintrustConfig { apiKey: string apiUrl?: string @@ -60,6 +62,7 @@ export interface SpanData { caller_functionname?: string caller_filename?: string caller_lineno?: number + span_origin?: Record } span_attributes?: { name?: string @@ -68,6 +71,62 @@ 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 || 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" } + 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.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 } + } + 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 +404,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, 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