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
42 changes: 41 additions & 1 deletion src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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)
})
})
62 changes: 61 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Braintrust API client for the OpenCode plugin
*/

import { PLUGIN_VERSION } from "./version"

export interface BraintrustConfig {
apiKey: string
apiUrl?: string
Expand Down Expand Up @@ -60,6 +62,7 @@ export interface SpanData {
caller_functionname?: string
caller_filename?: string
caller_lineno?: number
span_origin?: Record<string, unknown>
}
span_attributes?: {
name?: string
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -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
Loading