From 2e898add7e1fcea868fbeeff80c5e72eca4081cb Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 11:02:22 -0800 Subject: [PATCH 01/23] setup otel compat test in esm --- .github/workflows/js-sdk-deno-otel.yaml | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/js-sdk-deno-otel.yaml diff --git a/.github/workflows/js-sdk-deno-otel.yaml b/.github/workflows/js-sdk-deno-otel.yaml new file mode 100644 index 000000000..1437c02d8 --- /dev/null +++ b/.github/workflows/js-sdk-deno-otel.yaml @@ -0,0 +1,80 @@ +name: js-sdk-otel-esm-test + +on: + pull_request: + +permissions: + contents: read + +jobs: + test-esm-otel: + name: Test OTEL Distributed Tracing in ESM Env + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install SDK dependencies + working-directory: js + run: pnpm install + + - name: Install OTEL example dependencies + working-directory: js/examples/otel + run: npm install + + - name: Verify OTEL dependencies installed + working-directory: js/examples/otel + run: | + echo "Verifying OTEL packages installed..." + packages=( + "@opentelemetry/api" + "@opentelemetry/sdk-trace-base" + "@opentelemetry/sdk-node" + "@opentelemetry/context-async-hooks" + "tsx" + "openai" + ) + for package in "${packages[@]}"; do + if [ ! -d "node_modules/$package" ]; then + echo "❌ Package $package not found" + exit 1 + fi + echo "✅ $package verified" + done + echo "All required packages verified successfully" + + - name: Check if API key is available + id: check_api_key + run: | + if [ -z "${{ secrets.BRAINTRUST_API_KEY }}" ]; then + echo "api_key_available=false" >> $GITHUB_OUTPUT + echo "⚠️ BRAINTRUST_API_KEY secret not set - test will be skipped" + else + echo "api_key_available=true" >> $GITHUB_OUTPUT + fi + + - name: Run OTEL example with npm start + if: steps.check_api_key.outputs.api_key_available == 'true' + working-directory: js/examples/otel + env: + BRAINTRUST_OTEL_COMPAT: true + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + run: npm start + + - name: Display test results + if: always() + run: | + if [ "${{ steps.check_api_key.outputs.api_key_available }}" == "true" ]; then + echo "✅ ESM OTEL test completed" + else + echo "⚠️ Test skipped - BRAINTRUST_API_KEY secret not configured" + fi From 732f4d3047a1badca6bf07a32cb26feef47b92f5 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 11:20:46 -0800 Subject: [PATCH 02/23] dont use otel --- .github/workflows/js-sdk-deno-otel.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/js-sdk-deno-otel.yaml b/.github/workflows/js-sdk-deno-otel.yaml index 1437c02d8..47eba6785 100644 --- a/.github/workflows/js-sdk-deno-otel.yaml +++ b/.github/workflows/js-sdk-deno-otel.yaml @@ -66,7 +66,7 @@ jobs: if: steps.check_api_key.outputs.api_key_available == 'true' working-directory: js/examples/otel env: - BRAINTRUST_OTEL_COMPAT: true + BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} run: npm start From 5fb273d6fd15b3a77155ced42fdf48bf6834e923 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 13:26:05 -0800 Subject: [PATCH 03/23] add multiple tests and one to try cjs --- .github/workflows/js-sdk-cjs.yaml | 61 ++++ .github/workflows/js-sdk-esm.yaml | 51 +++ ...sdk-deno-otel.yaml => js-sdk-example.yaml} | 6 +- .github/workflows/js-sdk-node-example.yaml | 80 +++++ js/integration-tests/README.md | 77 ++++ .../distributed-tracing.test.ts | 222 ++++++++++++ js/integration-tests/nodesdk.test.ts | 339 ++++++++++++++++++ js/integration-tests/package.json | 27 ++ js/integration-tests/tsconfig.json | 12 + js/integration-tests/vitest.config.ts | 18 + 10 files changed, 890 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/js-sdk-cjs.yaml create mode 100644 .github/workflows/js-sdk-esm.yaml rename .github/workflows/{js-sdk-deno-otel.yaml => js-sdk-example.yaml} (95%) create mode 100644 .github/workflows/js-sdk-node-example.yaml create mode 100644 js/integration-tests/README.md create mode 100644 js/integration-tests/distributed-tracing.test.ts create mode 100644 js/integration-tests/nodesdk.test.ts create mode 100644 js/integration-tests/package.json create mode 100644 js/integration-tests/tsconfig.json create mode 100644 js/integration-tests/vitest.config.ts diff --git a/.github/workflows/js-sdk-cjs.yaml b/.github/workflows/js-sdk-cjs.yaml new file mode 100644 index 000000000..2cb353251 --- /dev/null +++ b/.github/workflows/js-sdk-cjs.yaml @@ -0,0 +1,61 @@ +name: JS SDK CJS OTEL Integration Tests +on: + pull_request: + paths: + - "sdk/js/**" + - ".github/workflows/js-sdk-cjs.yaml" + push: + branches: + - main + paths: + - "sdk/js/**" + - ".github/workflows/js-sdk-cjs.yaml" + workflow_dispatch: + +jobs: + otel-integration-tests-cjs: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: recursive + + - name: Install dependencies + uses: ./.github/actions/deps + + - name: Build braintrust SDK + shell: bash + run: | + eval "$(mise activate)" + cd sdk/js + pnpm build + + - name: Install integration test dependencies + shell: bash + run: | + eval "$(mise activate)" + cd sdk/js/integration-tests + pnpm install + + - name: Configure for CJS + shell: bash + run: | + cd sdk/js/integration-tests + # Update package.json to use CommonJS + node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); delete pkg.type; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));" + # Update tsconfig.json for CJS + node -e "const fs = require('fs'); const cfg = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')); cfg.compilerOptions.module = 'CommonJS'; cfg.compilerOptions.moduleResolution = 'node'; fs.writeFileSync('tsconfig.json', JSON.stringify(cfg, null, 2));" + + - name: Run OTEL integration tests (CJS) + shell: bash + env: + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + BRAINTRUST_OTEL_COMPAT: "true" + NODE_OPTIONS: "--require tsx/cjs" + run: | + eval "$(mise activate)" + cd sdk/js/integration-tests + pnpm test + timeout-minutes: 10 diff --git a/.github/workflows/js-sdk-esm.yaml b/.github/workflows/js-sdk-esm.yaml new file mode 100644 index 000000000..0de5cd898 --- /dev/null +++ b/.github/workflows/js-sdk-esm.yaml @@ -0,0 +1,51 @@ +name: JS SDK ESM OTEL Integration Tests +on: + pull_request: + paths: + - "sdk/js/**" + - ".github/workflows/js-sdk-esm.yaml" + push: + branches: + - main + paths: + - "sdk/js/**" + - ".github/workflows/js-sdk-esm.yaml" + workflow_dispatch: + +jobs: + otel-integration-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: recursive + + - name: Install dependencies + uses: ./.github/actions/deps + + - name: Build braintrust SDK + shell: bash + run: | + eval "$(mise activate)" + cd sdk/js + pnpm build + + - name: Install integration test dependencies + shell: bash + run: | + eval "$(mise activate)" + cd sdk/js/integration-tests + pnpm install + + - name: Run OTEL integration tests + shell: bash + env: + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + BRAINTRUST_OTEL_COMPAT: "true" + run: | + eval "$(mise activate)" + cd sdk/js/integration-tests + pnpm test + timeout-minutes: 10 diff --git a/.github/workflows/js-sdk-deno-otel.yaml b/.github/workflows/js-sdk-example.yaml similarity index 95% rename from .github/workflows/js-sdk-deno-otel.yaml rename to .github/workflows/js-sdk-example.yaml index 47eba6785..b8538efd7 100644 --- a/.github/workflows/js-sdk-deno-otel.yaml +++ b/.github/workflows/js-sdk-example.yaml @@ -1,4 +1,4 @@ -name: js-sdk-otel-esm-test +name: js-sdk-example on: pull_request: @@ -7,8 +7,8 @@ permissions: contents: read jobs: - test-esm-otel: - name: Test OTEL Distributed Tracing in ESM Env + test-example: + name: Test Braintrust Example runs-on: ubuntu-latest steps: diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml new file mode 100644 index 000000000..2035d1af1 --- /dev/null +++ b/.github/workflows/js-sdk-node-example.yaml @@ -0,0 +1,80 @@ +name: js-sdk-node-example + +on: + pull_request: + +permissions: + contents: read + +jobs: + test-node-example: + name: Test Braintrust Node Example + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install SDK dependencies + working-directory: js + run: pnpm install + + - name: Install OTEL example dependencies + working-directory: js/examples/otel + run: npm install + + - name: Verify OTEL dependencies installed + working-directory: js/examples/otel + run: | + echo "Verifying OTEL packages installed..." + packages=( + "@opentelemetry/api" + "@opentelemetry/sdk-trace-base" + "@opentelemetry/sdk-node" + "@opentelemetry/context-async-hooks" + "tsx" + "openai" + ) + for package in "${packages[@]}"; do + if [ ! -d "node_modules/$package" ]; then + echo "❌ Package $package not found" + exit 1 + fi + echo "✅ $package verified" + done + echo "All required packages verified successfully" + + - name: Check if API key is available + id: check_api_key + run: | + if [ -z "${{ secrets.BRAINTRUST_API_KEY }}" ]; then + echo "api_key_available=false" >> $GITHUB_OUTPUT + echo "⚠️ BRAINTRUST_API_KEY secret not set - test will be skipped" + else + echo "api_key_available=true" >> $GITHUB_OUTPUT + fi + + - name: Configure for CJS + shell: bash + run: | + cd sdk/js/integration-tests + # Update package.json to use CommonJS + node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); delete pkg.type; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));" + # Update tsconfig.json for CJS + node -e "const fs = require('fs'); const cfg = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')); cfg.compilerOptions.module = 'CommonJS'; cfg.compilerOptions.moduleResolution = 'node'; fs.writeFileSync('tsconfig.json', JSON.stringify(cfg, null, 2));" + + - name: Run OTEL example with npm start + if: steps.check_api_key.outputs.api_key_available == 'true' + working-directory: js/examples/otel + env: + BRAINTRUST_OTEL_COMPAT: false + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + run: npm start diff --git a/js/integration-tests/README.md b/js/integration-tests/README.md new file mode 100644 index 000000000..cc0972f60 --- /dev/null +++ b/js/integration-tests/README.md @@ -0,0 +1,77 @@ +# Braintrust OTEL Integration Tests + +This directory contains integration tests for Braintrust's OpenTelemetry (OTEL) integration features. + +## Overview + +The integration tests verify that Braintrust works correctly with OpenTelemetry SDKs and distributed tracing scenarios. + +## Test Files + +- **`distributed-tracing.test.ts`**: Tests distributed tracing flows where Braintrust spans, OTEL spans, and Braintrust spans interact across service boundaries (BT → OTEL → BT). +- **`nodesdk.test.ts`**: Tests the integration of BraintrustSpanProcessor with OpenTelemetry NodeSDK, including span filtering and AI span detection. + +## Running Tests + +### Locally + +```bash +# Install dependencies +cd sdk/js/integration-tests +pnpm install + +# Run all tests +pnpm test + +# Watch mode +pnpm test:watch +``` + +### Environment Variables + +The tests require: + +- `BRAINTRUST_API_KEY`: Your Braintrust API key +- `BRAINTRUST_OTEL_COMPAT`: Set to `"true"` for distributed tracing tests + +### CI/CD + +Tests are automatically run in CI via two workflows: + +**ESM Tests** (`.github/workflows/js-sdk-esm.yaml`): + +- Tests the SDK in ES Module mode +- Pull requests that modify `sdk/js/**` +- Pushes to the `main` branch + +**CJS Tests** (`.github/workflows/js-sdk-cjs.yaml`): + +- Tests the SDK in CommonJS mode +- Pull requests that modify `sdk/js/**` +- Pushes to the `main` branch + +## Test Structure + +Tests use: + +- **Vitest** as the test runner +- **@opentelemetry/api** for OTEL API interactions +- **@opentelemetry/sdk-node** for NodeSDK integration +- **@opentelemetry/sdk-trace-base** for tracer providers +- **@opentelemetry/context-async-hooks** for context management +- **braintrust** SDK for logging and span management + +## Key Features Tested + +1. **Distributed Tracing**: Verifies that spans can be propagated across service boundaries +2. **Context Propagation**: Tests that OTEL context is correctly maintained through async operations +3. **Span Filtering**: Validates that AI-related spans are correctly identified and filtered +4. **NodeSDK Integration**: Ensures BraintrustSpanProcessor works with OpenTelemetry NodeSDK +5. **Parallel Operations**: Tests concurrent span creation and context handling +6. **Background Logger Verification**: Tests validate span data by draining and inspecting the background logger + +## Notes + +- Tests have a 30-second timeout to account for network operations +- Integration tests connect to the Braintrust API, so they require valid credentials +- All tests flush spans and wait for processing to ensure data is sent to Braintrust diff --git a/js/integration-tests/distributed-tracing.test.ts b/js/integration-tests/distributed-tracing.test.ts new file mode 100644 index 000000000..2006d7251 --- /dev/null +++ b/js/integration-tests/distributed-tracing.test.ts @@ -0,0 +1,222 @@ +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + afterEach, +} from "vitest"; +import * as api from "@opentelemetry/api"; +import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; +import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; +import { + initLogger, + login, + otel, + BraintrustSpanProcessor, + _exportsForTestingOnly, +} from "braintrust"; + +const { trace, context, propagation } = api; + +describe("Distributed Tracing: BT → OTEL → BT", () => { + let provider: BasicTracerProvider; + let tracer: api.Tracer; + let contextManager: AsyncHooksContextManager; + let logger: ReturnType; + let testLogger: ReturnType< + typeof _exportsForTestingOnly.useTestBackgroundLogger + >; + + beforeAll(async () => { + process.env.BRAINTRUST_OTEL_COMPAT = "true"; + + provider = new BasicTracerProvider({ + spanProcessors: [new BraintrustSpanProcessor()], + }); + trace.setGlobalTracerProvider(provider); + tracer = trace.getTracer("service-b"); + + contextManager = new AsyncHooksContextManager(); + contextManager.enable(); + context.setGlobalContextManager(contextManager); + + await _exportsForTestingOnly.simulateLoginForTests(); + logger = initLogger({ + projectName: "otel-integration-tests", + projectId: "test-project-id", + }); + }); + + beforeEach(() => { + testLogger = _exportsForTestingOnly.useTestBackgroundLogger(); + }); + + afterEach(() => { + _exportsForTestingOnly.clearTestBackgroundLogger(); + }); + + afterAll(async () => { + await logger.flush(); + await provider.forceFlush(); + contextManager.disable(); + }); + + it("should successfully trace across BT → OTEL → BT services", async () => { + let spanLink = ""; + let spanCId: string | undefined; + + await logger.traced(async (spanA) => { + spanLink = spanA.link(); + const exported = await spanA.export(); + + expect(exported).toBeDefined(); + expect(typeof exported).toBe("string"); + + const ctx = otel.contextFromSpanExport(exported); + await context.with(ctx, async () => { + await tracer.startActiveSpan("service_b", async (spanB: api.Span) => { + const currentCtx = otel.addSpanParentToBaggage(spanB); + const headers: Record = {}; + propagation.inject(currentCtx, headers); + + expect(headers).toBeDefined(); + expect(Object.keys(headers).length).toBeGreaterThan(0); + + const parent = otel.parentFromHeaders(headers); + await logger.traced( + async (spanC) => { + spanC.log({ input: { message: "from service B" } }); + spanCId = spanC.id; + }, + { name: "service_c", parent }, + ); + + spanB.end(); + }); + }); + }); + + expect(spanLink).toBeTruthy(); + expect(spanCId).toBeDefined(); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(2); + + const spanNames = spans.map((s: any) => s.span_attributes?.name); + expect(spanNames).toContain("service_c"); + + const serviceCSpan = spans.find( + (s: any) => s.span_attributes?.name === "service_c", + ); + expect(serviceCSpan).toBeDefined(); + expect(serviceCSpan.input).toEqual({ message: "from service B" }); + }, 30000); + + it("should propagate context correctly through OTEL middleware", async () => { + const results: string[] = []; + + await logger.traced(async (spanA) => { + results.push("spanA"); + spanA.log({ input: { service: "A" } }); + + const exported = await spanA.export(); + const ctx = otel.contextFromSpanExport(exported); + + await context.with(ctx, async () => { + await tracer.startActiveSpan( + "service_b_operation", + async (spanB: api.Span) => { + results.push("spanB"); + spanB.setAttribute("operation", "process"); + + const currentCtx = otel.addSpanParentToBaggage(spanB); + const headers: Record = {}; + propagation.inject(currentCtx, headers); + + const parent = otel.parentFromHeaders(headers); + await logger.traced( + async (spanC) => { + results.push("spanC"); + spanC.log({ output: { result: "processed" } }); + }, + { name: "service_c_operation", parent }, + ); + + spanB.end(); + }, + ); + }); + }); + + expect(results).toEqual(["spanA", "spanB", "spanC"]); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(2); + + const serviceASpan = spans.find((s: any) => s.input?.service === "A"); + expect(serviceASpan).toBeDefined(); + + const serviceCSpan = spans.find( + (s: any) => s.span_attributes?.name === "service_c_operation", + ); + expect(serviceCSpan).toBeDefined(); + expect(serviceCSpan.output).toEqual({ result: "processed" }); + }, 30000); + + it("should handle multiple parallel OTEL operations", async () => { + const operations = ["op1", "op2", "op3"]; + const completedOps: string[] = []; + + await logger.traced(async (spanA) => { + const exported = await spanA.export(); + const ctx = otel.contextFromSpanExport(exported); + + await Promise.all( + operations.map((op) => + context.with(ctx, async () => { + await tracer.startActiveSpan( + `service_b_${op}`, + async (spanB: api.Span) => { + const currentCtx = otel.addSpanParentToBaggage(spanB); + const headers: Record = {}; + propagation.inject(currentCtx, headers); + + const parent = otel.parentFromHeaders(headers); + await logger.traced( + async (spanC) => { + spanC.log({ input: { operation: op } }); + completedOps.push(op); + }, + { name: `service_c_${op}`, parent }, + ); + + spanB.end(); + }, + ); + }), + ), + ); + }); + + expect(completedOps.sort()).toEqual(operations.sort()); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(3); + + const serviceCSpans = spans.filter((s: any) => + s.span_attributes?.name?.startsWith("service_c_"), + ); + expect(serviceCSpans.length).toBe(3); + + const loggedOps = serviceCSpans.map((s: any) => s.input?.operation).sort(); + expect(loggedOps).toEqual(operations.sort()); + }, 30000); +}); diff --git a/js/integration-tests/nodesdk.test.ts b/js/integration-tests/nodesdk.test.ts new file mode 100644 index 000000000..13269c2c8 --- /dev/null +++ b/js/integration-tests/nodesdk.test.ts @@ -0,0 +1,339 @@ +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + afterEach, +} from "vitest"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { BraintrustSpanProcessor, _exportsForTestingOnly } from "braintrust"; +import { trace } from "@opentelemetry/api"; + +describe("NodeSDK with BraintrustSpanProcessor", () => { + let sdk: NodeSDK; + let testLogger: ReturnType< + typeof _exportsForTestingOnly.useTestBackgroundLogger + >; + + beforeAll(async () => { + await _exportsForTestingOnly.simulateLoginForTests(); + + sdk = new NodeSDK({ + serviceName: "integration-test-service", + spanProcessor: new BraintrustSpanProcessor({ + parent: "project_name:otel-integration-tests", + filterAISpans: true, + }), + }); + + sdk.start(); + }); + + beforeEach(() => { + testLogger = _exportsForTestingOnly.useTestBackgroundLogger(); + }); + + afterEach(() => { + _exportsForTestingOnly.clearTestBackgroundLogger(); + }); + + afterAll(async () => { + await sdk.shutdown(); + }); + + it("should initialize NodeSDK with BraintrustSpanProcessor", () => { + expect(sdk).toBeDefined(); + const tracer = trace.getTracer("test-service", "1.0.0"); + expect(tracer).toBeDefined(); + }); + + it("should create and export root spans", async () => { + const tracer = trace.getTracer("test-service", "1.0.0"); + + await tracer.startActiveSpan("test_root_span", async (rootSpan) => { + rootSpan.setAttributes({ + "test.type": "integration", + "test.timestamp": new Date().toISOString(), + }); + + expect(rootSpan.spanContext().traceId).toBeTruthy(); + expect(rootSpan.spanContext().spanId).toBeTruthy(); + + rootSpan.end(); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(1); + + const rootSpan = spans.find( + (s: any) => s.span_attributes?.name === "test_root_span", + ); + expect(rootSpan).toBeDefined(); + expect(rootSpan.metadata?.["test.type"]).toBe("integration"); + }, 15000); + + it("should filter AI spans when filterAISpans is enabled", async () => { + const tracer = trace.getTracer("test-service", "1.0.0"); + + await tracer.startActiveSpan("test_filter_ai", async (rootSpan) => { + rootSpan.setAttributes({ + "test.id": "filter_test", + }); + + await tracer.startActiveSpan( + "gen_ai.chat.completions", + async (aiSpan) => { + aiSpan.setAttributes({ + "gen_ai.system": "openai", + "gen_ai.operation.name": "chat.completions", + "gen_ai.request.model": "gpt-4", + }); + + expect(aiSpan.spanContext().spanId).toBeTruthy(); + aiSpan.end(); + }, + ); + + await tracer.startActiveSpan("non_ai_operation", async (nonAiSpan) => { + nonAiSpan.setAttributes({ + "operation.type": "database", + }); + + expect(nonAiSpan.spanContext().spanId).toBeTruthy(); + nonAiSpan.end(); + }); + + rootSpan.end(); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(2); + + const spanNames = spans.map((s: any) => s.span_attributes?.name); + expect(spanNames).toContain("test_filter_ai"); + expect(spanNames).toContain("gen_ai.chat.completions"); + + const aiSpan = spans.find( + (s: any) => s.span_attributes?.name === "gen_ai.chat.completions", + ); + expect(aiSpan).toBeDefined(); + expect(aiSpan.metadata?.["gen_ai.system"]).toBe("openai"); + }, 15000); + + it("should handle nested spans correctly", async () => { + const tracer = trace.getTracer("test-service", "1.0.0"); + const spanIds: string[] = []; + + await tracer.startActiveSpan("parent_operation", async (parentSpan) => { + spanIds.push(parentSpan.spanContext().spanId); + + parentSpan.setAttributes({ + operation: "parent", + level: 0, + }); + + await tracer.startActiveSpan("child_operation_1", async (child1) => { + spanIds.push(child1.spanContext().spanId); + child1.setAttributes({ + operation: "child1", + level: 1, + }); + + await tracer.startActiveSpan( + "grandchild_operation", + async (grandchild) => { + spanIds.push(grandchild.spanContext().spanId); + grandchild.setAttributes({ + operation: "grandchild", + level: 2, + }); + grandchild.end(); + }, + ); + + child1.end(); + }); + + await tracer.startActiveSpan("child_operation_2", async (child2) => { + spanIds.push(child2.spanContext().spanId); + child2.setAttributes({ + operation: "child2", + level: 1, + }); + child2.end(); + }); + + parentSpan.end(); + }); + + expect(spanIds).toHaveLength(4); + expect(new Set(spanIds).size).toBe(4); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(4); + + const parentSpan = spans.find( + (s: any) => s.metadata?.operation === "parent", + ); + const child1Span = spans.find( + (s: any) => s.metadata?.operation === "child1", + ); + const grandchildSpan = spans.find( + (s: any) => s.metadata?.operation === "grandchild", + ); + const child2Span = spans.find( + (s: any) => s.metadata?.operation === "child2", + ); + + expect(parentSpan).toBeDefined(); + expect(child1Span).toBeDefined(); + expect(grandchildSpan).toBeDefined(); + expect(child2Span).toBeDefined(); + + expect(parentSpan.metadata?.level).toBe(0); + expect(child1Span.metadata?.level).toBe(1); + expect(grandchildSpan.metadata?.level).toBe(2); + expect(child2Span.metadata?.level).toBe(1); + }, 15000); + + it("should capture span attributes correctly", async () => { + const tracer = trace.getTracer("test-service", "1.0.0"); + + await tracer.startActiveSpan("attribute_test", async (span) => { + const testAttributes = { + "string.attr": "test_value", + "number.attr": 42, + "boolean.attr": true, + "timestamp.attr": new Date().toISOString(), + }; + + span.setAttributes(testAttributes); + + expect(span.spanContext().traceId).toBeTruthy(); + + span.end(); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(1); + + const attrSpan = spans.find( + (s: any) => s.span_attributes?.name === "attribute_test", + ); + expect(attrSpan).toBeDefined(); + expect(attrSpan.metadata?.["string.attr"]).toBe("test_value"); + expect(attrSpan.metadata?.["number.attr"]).toBe(42); + expect(attrSpan.metadata?.["boolean.attr"]).toBe(true); + }, 15000); + + it("should handle AI-related span prefixes", async () => { + const tracer = trace.getTracer("test-service", "1.0.0"); + const aiPrefixes = [ + "gen_ai.completion", + "llm.generate", + "ai.model_call", + "braintrust.eval", + "traceloop.workflow", + ]; + + await tracer.startActiveSpan("ai_prefix_test_root", async (rootSpan) => { + for (const prefix of aiPrefixes) { + await tracer.startActiveSpan(prefix, async (aiSpan) => { + aiSpan.setAttributes({ + "test.prefix": prefix, + "test.type": "ai_span", + }); + + expect(aiSpan.spanContext().spanId).toBeTruthy(); + aiSpan.end(); + }); + } + + rootSpan.end(); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(6); + + const rootSpan = spans.find( + (s: any) => s.span_attributes?.name === "ai_prefix_test_root", + ); + expect(rootSpan).toBeDefined(); + + for (const prefix of aiPrefixes) { + const aiSpan = spans.find((s: any) => s.span_attributes?.name === prefix); + expect(aiSpan).toBeDefined(); + expect(aiSpan.metadata?.["test.prefix"]).toBe(prefix); + expect(aiSpan.metadata?.["test.type"]).toBe("ai_span"); + } + }, 15000); + + it("should support parallel span creation", async () => { + const tracer = trace.getTracer("test-service", "1.0.0"); + + await tracer.startActiveSpan("parallel_test_root", async (rootSpan) => { + const operations = Array.from({ length: 5 }, (_, i) => i); + + await Promise.all( + operations.map(async (i) => { + await tracer.startActiveSpan(`parallel_op_${i}`, async (span) => { + span.setAttributes({ + "operation.id": i, + "operation.type": "parallel", + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + span.end(); + }); + }), + ); + + rootSpan.end(); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await testLogger.flush(); + const spans = await testLogger.drain(); + + expect(spans.length).toBeGreaterThanOrEqual(6); + + const rootSpan = spans.find( + (s: any) => s.span_attributes?.name === "parallel_test_root", + ); + expect(rootSpan).toBeDefined(); + + const parallelSpans = spans.filter((s: any) => + s.span_attributes?.name?.startsWith("parallel_op_"), + ); + expect(parallelSpans.length).toBe(5); + + parallelSpans.forEach((span: any) => { + expect(span.metadata?.["operation.type"]).toBe("parallel"); + expect(typeof span.metadata?.["operation.id"]).toBe("number"); + }); + }, 15000); +}); diff --git a/js/integration-tests/package.json b/js/integration-tests/package.json new file mode 100644 index 000000000..3d542e786 --- /dev/null +++ b/js/integration-tests/package.json @@ -0,0 +1,27 @@ +{ + "name": "braintrust-otel-integration-tests", + "version": "1.0.0", + "description": "Integration tests for Braintrust OpenTelemetry integration", + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "start:node": "node nodesdk.test.ts", + "start:otel": "node otel.test.ts" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.1", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-base": "^2.0.1", + "@opentelemetry/sdk-trace-node": "^2.0.1", + "braintrust": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "typescript": "^5.4.4", + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^2.1.9" + } +} diff --git a/js/integration-tests/tsconfig.json b/js/integration-tests/tsconfig.json new file mode 100644 index 000000000..d120075dc --- /dev/null +++ b/js/integration-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "lib": ["ES2022"], + "types": ["node", "vitest/globals"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/js/integration-tests/vitest.config.ts b/js/integration-tests/vitest.config.ts new file mode 100644 index 000000000..b773ee8ef --- /dev/null +++ b/js/integration-tests/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + environment: "node", + testTimeout: 30000, + hookTimeout: 30000, + globals: false, + include: ["**/*.test.ts"], + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/.{idea,git,cache,output,temp}/**", + ], + }, +}); From f9bda3d9e7d23b7dcc163622b0ccf7628a4145b0 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 13:43:57 -0800 Subject: [PATCH 04/23] fix github actions --- .github/workflows/js-sdk-cjs.yaml | 18 ++++++++++++---- .github/workflows/js-sdk-node-example.yaml | 2 +- js/integration-tests/package-cjs.json | 25 ++++++++++++++++++++++ js/integration-tests/tsconfig-cjs.json | 13 +++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 js/integration-tests/package-cjs.json create mode 100644 js/integration-tests/tsconfig-cjs.json diff --git a/.github/workflows/js-sdk-cjs.yaml b/.github/workflows/js-sdk-cjs.yaml index 2cb353251..ea39c3bd2 100644 --- a/.github/workflows/js-sdk-cjs.yaml +++ b/.github/workflows/js-sdk-cjs.yaml @@ -43,10 +43,11 @@ jobs: shell: bash run: | cd sdk/js/integration-tests - # Update package.json to use CommonJS - node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); delete pkg.type; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));" - # Update tsconfig.json for CJS - node -e "const fs = require('fs'); const cfg = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')); cfg.compilerOptions.module = 'CommonJS'; cfg.compilerOptions.moduleResolution = 'node'; fs.writeFileSync('tsconfig.json', JSON.stringify(cfg, null, 2));" + # Backup original files and use CJS versions + mv package.json package-esm.json.bak + mv tsconfig.json tsconfig-esm.json.bak + cp package-cjs.json package.json + cp tsconfig-cjs.json tsconfig.json - name: Run OTEL integration tests (CJS) shell: bash @@ -59,3 +60,12 @@ jobs: cd sdk/js/integration-tests pnpm test timeout-minutes: 10 + + - name: Restore original config files + if: always() + shell: bash + run: | + cd sdk/js/integration-tests + # Restore original files + mv package-esm.json.bak package.json || true + mv tsconfig-esm.json.bak tsconfig.json || true diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index 2035d1af1..170da06a1 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -65,7 +65,7 @@ jobs: - name: Configure for CJS shell: bash run: | - cd sdk/js/integration-tests + cd sdk/js/examples/otel # Update package.json to use CommonJS node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); delete pkg.type; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));" # Update tsconfig.json for CJS diff --git a/js/integration-tests/package-cjs.json b/js/integration-tests/package-cjs.json new file mode 100644 index 000000000..35d8674a3 --- /dev/null +++ b/js/integration-tests/package-cjs.json @@ -0,0 +1,25 @@ +{ + "$comment": "This is the CommonJS configuration used by .github/workflows/js-sdk-cjs.yaml - DO NOT remove 'type' field", + "name": "braintrust-otel-integration-tests", + "version": "1.0.0", + "description": "Integration tests for Braintrust OpenTelemetry integration (CommonJS)", + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.1", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-base": "^2.0.1", + "@opentelemetry/sdk-trace-node": "^2.0.1", + "braintrust": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "typescript": "^5.4.4", + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^2.1.9" + } +} diff --git a/js/integration-tests/tsconfig-cjs.json b/js/integration-tests/tsconfig-cjs.json new file mode 100644 index 000000000..37bcf8421 --- /dev/null +++ b/js/integration-tests/tsconfig-cjs.json @@ -0,0 +1,13 @@ +{ + "$comment": "This is the CommonJS tsconfig used by .github/workflows/js-sdk-cjs.yaml", + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES2022", + "lib": ["ES2022"], + "types": ["node", "vitest/globals"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} From 68689d6d2f8925781654165e346feac8528058c3 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 13:48:06 -0800 Subject: [PATCH 05/23] fix tests --- .github/workflows/js-sdk-cjs.yaml | 9 --- .github/workflows/js-sdk-node-example.yaml | 25 +++++-- js/examples/otel/package-cjs.json | 28 ++++++++ js/examples/otel/tsconfig-cjs.json | 15 +++++ js/integration-tests/README.md | 77 ---------------------- 5 files changed, 62 insertions(+), 92 deletions(-) create mode 100644 js/examples/otel/package-cjs.json create mode 100644 js/examples/otel/tsconfig-cjs.json delete mode 100644 js/integration-tests/README.md diff --git a/.github/workflows/js-sdk-cjs.yaml b/.github/workflows/js-sdk-cjs.yaml index ea39c3bd2..bd3d5545d 100644 --- a/.github/workflows/js-sdk-cjs.yaml +++ b/.github/workflows/js-sdk-cjs.yaml @@ -60,12 +60,3 @@ jobs: cd sdk/js/integration-tests pnpm test timeout-minutes: 10 - - - name: Restore original config files - if: always() - shell: bash - run: | - cd sdk/js/integration-tests - # Restore original files - mv package-esm.json.bak package.json || true - mv tsconfig-esm.json.bak tsconfig.json || true diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index 170da06a1..eb37f5836 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -63,13 +63,16 @@ jobs: fi - name: Configure for CJS - shell: bash + working-directory: js/examples/otel run: | - cd sdk/js/examples/otel - # Update package.json to use CommonJS - node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); delete pkg.type; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));" - # Update tsconfig.json for CJS - node -e "const fs = require('fs'); const cfg = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')); cfg.compilerOptions.module = 'CommonJS'; cfg.compilerOptions.moduleResolution = 'node'; fs.writeFileSync('tsconfig.json', JSON.stringify(cfg, null, 2));" + # Backup original files and use CJS versions + mv package.json package-esm.json.bak + cp package-cjs.json package.json + # Only swap tsconfig if it exists + if [ -f tsconfig.json ]; then + mv tsconfig.json tsconfig-esm.json.bak + fi + cp tsconfig-cjs.json tsconfig.json - name: Run OTEL example with npm start if: steps.check_api_key.outputs.api_key_available == 'true' @@ -78,3 +81,13 @@ jobs: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} run: npm start + + - name: Restore original config files + if: always() + working-directory: js/examples/otel + run: | + # Restore original files + mv package-esm.json.bak package.json || true + if [ -f tsconfig-esm.json.bak ]; then + mv tsconfig-esm.json.bak tsconfig.json || true + fi diff --git a/js/examples/otel/package-cjs.json b/js/examples/otel/package-cjs.json new file mode 100644 index 000000000..2be0374fa --- /dev/null +++ b/js/examples/otel/package-cjs.json @@ -0,0 +1,28 @@ +{ + "$comment": "This is the CommonJS configuration used by .github/workflows/js-sdk-node-example.yaml", + "name": "braintrust-otel-example", + "version": "1.0.0", + "type": "commonjs", + "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK (CommonJS)", + "main": "nodesdk_example.ts", + "scripts": { + "start": "tsx nodesdk_example.ts" + }, + "dependencies": { + "@traceloop/instrumentation-openai": "^0.14.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.62.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-base": "^2.0.1", + "@opentelemetry/sdk-trace-node": "^2.0.1", + "@opentelemetry/semantic-conventions": "^1.36.0", + "openai": "^4.73.1", + "tsx": "^4.19.2" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "typescript": "^5.4.4" + } +} diff --git a/js/examples/otel/tsconfig-cjs.json b/js/examples/otel/tsconfig-cjs.json new file mode 100644 index 000000000..84aa07ce7 --- /dev/null +++ b/js/examples/otel/tsconfig-cjs.json @@ -0,0 +1,15 @@ +{ + "$comment": "This is the CommonJS tsconfig used by .github/workflows/js-sdk-node-example.yaml", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES2022", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/js/integration-tests/README.md b/js/integration-tests/README.md deleted file mode 100644 index cc0972f60..000000000 --- a/js/integration-tests/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Braintrust OTEL Integration Tests - -This directory contains integration tests for Braintrust's OpenTelemetry (OTEL) integration features. - -## Overview - -The integration tests verify that Braintrust works correctly with OpenTelemetry SDKs and distributed tracing scenarios. - -## Test Files - -- **`distributed-tracing.test.ts`**: Tests distributed tracing flows where Braintrust spans, OTEL spans, and Braintrust spans interact across service boundaries (BT → OTEL → BT). -- **`nodesdk.test.ts`**: Tests the integration of BraintrustSpanProcessor with OpenTelemetry NodeSDK, including span filtering and AI span detection. - -## Running Tests - -### Locally - -```bash -# Install dependencies -cd sdk/js/integration-tests -pnpm install - -# Run all tests -pnpm test - -# Watch mode -pnpm test:watch -``` - -### Environment Variables - -The tests require: - -- `BRAINTRUST_API_KEY`: Your Braintrust API key -- `BRAINTRUST_OTEL_COMPAT`: Set to `"true"` for distributed tracing tests - -### CI/CD - -Tests are automatically run in CI via two workflows: - -**ESM Tests** (`.github/workflows/js-sdk-esm.yaml`): - -- Tests the SDK in ES Module mode -- Pull requests that modify `sdk/js/**` -- Pushes to the `main` branch - -**CJS Tests** (`.github/workflows/js-sdk-cjs.yaml`): - -- Tests the SDK in CommonJS mode -- Pull requests that modify `sdk/js/**` -- Pushes to the `main` branch - -## Test Structure - -Tests use: - -- **Vitest** as the test runner -- **@opentelemetry/api** for OTEL API interactions -- **@opentelemetry/sdk-node** for NodeSDK integration -- **@opentelemetry/sdk-trace-base** for tracer providers -- **@opentelemetry/context-async-hooks** for context management -- **braintrust** SDK for logging and span management - -## Key Features Tested - -1. **Distributed Tracing**: Verifies that spans can be propagated across service boundaries -2. **Context Propagation**: Tests that OTEL context is correctly maintained through async operations -3. **Span Filtering**: Validates that AI-related spans are correctly identified and filtered -4. **NodeSDK Integration**: Ensures BraintrustSpanProcessor works with OpenTelemetry NodeSDK -5. **Parallel Operations**: Tests concurrent span creation and context handling -6. **Background Logger Verification**: Tests validate span data by draining and inspecting the background logger - -## Notes - -- Tests have a 30-second timeout to account for network operations -- Integration tests connect to the Braintrust API, so they require valid credentials -- All tests flush spans and wait for processing to ensure data is sent to Braintrust From d469057fed7a805591747f0bf55d6314e6bf0759 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 14:01:19 -0800 Subject: [PATCH 06/23] attempt to force cjs --- .github/workflows/js-sdk-cjs.yaml | 10 ------ .github/workflows/js-sdk-esm.yaml | 10 ------ .github/workflows/js-sdk-node-example.yaml | 38 ++++++++++++++-------- js/examples/otel/package-cjs.json | 4 +-- js/examples/otel/tsconfig-cjs.json | 16 +++++---- 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/.github/workflows/js-sdk-cjs.yaml b/.github/workflows/js-sdk-cjs.yaml index bd3d5545d..382ab9162 100644 --- a/.github/workflows/js-sdk-cjs.yaml +++ b/.github/workflows/js-sdk-cjs.yaml @@ -1,16 +1,6 @@ name: JS SDK CJS OTEL Integration Tests on: pull_request: - paths: - - "sdk/js/**" - - ".github/workflows/js-sdk-cjs.yaml" - push: - branches: - - main - paths: - - "sdk/js/**" - - ".github/workflows/js-sdk-cjs.yaml" - workflow_dispatch: jobs: otel-integration-tests-cjs: diff --git a/.github/workflows/js-sdk-esm.yaml b/.github/workflows/js-sdk-esm.yaml index 0de5cd898..0ff2cc4a3 100644 --- a/.github/workflows/js-sdk-esm.yaml +++ b/.github/workflows/js-sdk-esm.yaml @@ -1,16 +1,6 @@ name: JS SDK ESM OTEL Integration Tests on: pull_request: - paths: - - "sdk/js/**" - - ".github/workflows/js-sdk-esm.yaml" - push: - branches: - - main - paths: - - "sdk/js/**" - - ".github/workflows/js-sdk-esm.yaml" - workflow_dispatch: jobs: otel-integration-tests: diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index eb37f5836..94904716d 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -27,6 +27,27 @@ jobs: working-directory: js run: pnpm install + - name: Build SDK + working-directory: js + run: npm run build + + - name: Configure for CJS and update imports + working-directory: js/examples/otel + run: | + # Backup original files and use CJS versions + mv package.json package-esm.json.bak + cp package-cjs.json package.json + # Only swap tsconfig if it exists + if [ -f tsconfig.json ]; then + mv tsconfig.json tsconfig-esm.json.bak + fi + cp tsconfig-cjs.json tsconfig.json + + # Update import to use compiled CJS dist files + sed -i 's|from "../../src"|from "../../dist/index.js"|g' nodesdk_example.ts + echo "Updated nodesdk_example.ts to use compiled CJS files" + cat nodesdk_example.ts | grep "import.*BraintrustSpanProcessor" + - name: Install OTEL example dependencies working-directory: js/examples/otel run: npm install @@ -40,8 +61,8 @@ jobs: "@opentelemetry/sdk-trace-base" "@opentelemetry/sdk-node" "@opentelemetry/context-async-hooks" - "tsx" "openai" + "ts-node" ) for package in "${packages[@]}"; do if [ ! -d "node_modules/$package" ]; then @@ -62,24 +83,13 @@ jobs: echo "api_key_available=true" >> $GITHUB_OUTPUT fi - - name: Configure for CJS - working-directory: js/examples/otel - run: | - # Backup original files and use CJS versions - mv package.json package-esm.json.bak - cp package-cjs.json package.json - # Only swap tsconfig if it exists - if [ -f tsconfig.json ]; then - mv tsconfig.json tsconfig-esm.json.bak - fi - cp tsconfig-cjs.json tsconfig.json - - name: Run OTEL example with npm start if: steps.check_api_key.outputs.api_key_available == 'true' working-directory: js/examples/otel env: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + TS_NODE_PROJECT: ./tsconfig.json run: npm start - name: Restore original config files @@ -91,3 +101,5 @@ jobs: if [ -f tsconfig-esm.json.bak ]; then mv tsconfig-esm.json.bak tsconfig.json || true fi + # Restore original imports + git checkout nodesdk_example.ts || true diff --git a/js/examples/otel/package-cjs.json b/js/examples/otel/package-cjs.json index 2be0374fa..59dd8fa53 100644 --- a/js/examples/otel/package-cjs.json +++ b/js/examples/otel/package-cjs.json @@ -6,7 +6,7 @@ "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK (CommonJS)", "main": "nodesdk_example.ts", "scripts": { - "start": "tsx nodesdk_example.ts" + "start": "TS_NODE_TRANSPILE_ONLY=true node -r ts-node/register nodesdk_example.ts" }, "dependencies": { "@traceloop/instrumentation-openai": "^0.14.0", @@ -19,7 +19,7 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "openai": "^4.73.1", - "tsx": "^4.19.2" + "ts-node": "^10.9.2" }, "devDependencies": { "@types/node": "^20.10.5", diff --git a/js/examples/otel/tsconfig-cjs.json b/js/examples/otel/tsconfig-cjs.json index 84aa07ce7..05bb40b20 100644 --- a/js/examples/otel/tsconfig-cjs.json +++ b/js/examples/otel/tsconfig-cjs.json @@ -3,13 +3,17 @@ "compilerOptions": { "module": "CommonJS", "moduleResolution": "node", - "target": "ES2022", - "lib": ["ES2022"], - "strict": true, + "target": "ES2020", + "lib": ["ES2020"], "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true }, - "include": ["**/*.ts"], - "exclude": ["node_modules"] + "ts-node": { + "transpileOnly": true, + "compilerOptions": { + "module": "CommonJS" + } + } } From fc18afa33286a54f0cd969d05c0d3866f24b7ea9 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 14:54:17 -0800 Subject: [PATCH 07/23] modify example to run in cjs --- .github/workflows/js-sdk-node-example.yaml | 13 ++----------- js/examples/otel/distributed-tracing.ts | 14 ++++++++++++++ js/examples/otel/package-cjs.json | 6 +++--- .../{ => cjs}/distributed-tracing.test.ts | 0 js/integration-tests/{ => esm}/nodesdk.test.ts | 0 5 files changed, 19 insertions(+), 14 deletions(-) rename js/integration-tests/{ => cjs}/distributed-tracing.test.ts (100%) rename js/integration-tests/{ => esm}/nodesdk.test.ts (100%) diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index 94904716d..8222b08d4 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -21,15 +21,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" - - - name: Install SDK dependencies - working-directory: js - run: pnpm install - - - name: Build SDK - working-directory: js - run: npm run build + node-version: "14" - name: Configure for CJS and update imports working-directory: js/examples/otel @@ -62,7 +54,7 @@ jobs: "@opentelemetry/sdk-node" "@opentelemetry/context-async-hooks" "openai" - "ts-node" + "tsx" ) for package in "${packages[@]}"; do if [ ! -d "node_modules/$package" ]; then @@ -89,7 +81,6 @@ jobs: env: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - TS_NODE_PROJECT: ./tsconfig.json run: npm start - name: Restore original config files diff --git a/js/examples/otel/distributed-tracing.ts b/js/examples/otel/distributed-tracing.ts index 40f81193e..8bcbdaad0 100755 --- a/js/examples/otel/distributed-tracing.ts +++ b/js/examples/otel/distributed-tracing.ts @@ -13,6 +13,7 @@ import { login, otel, BraintrustSpanProcessor, + BraintrustExporter, } from "../../dist/index.js"; const { trace, context, propagation } = api; @@ -67,6 +68,19 @@ async function main() { await new Promise((resolve) => setTimeout(resolve, 2000)); console.log(`\nView trace: ${spanLink}`); + await register(); +} + +async function register() { + // Dynamically import ESM-only @vercel/otel module + const { registerOTel } = await import("@vercel/otel"); + registerOTel({ + serviceName: "my-braintrust-app", + traceExporter: new BraintrustExporter({ + filterAISpans: true, + parent: `project_name:${process.env.PROJECT_NAME}`, + }), + }); } main().catch(console.error); diff --git a/js/examples/otel/package-cjs.json b/js/examples/otel/package-cjs.json index 59dd8fa53..af0931140 100644 --- a/js/examples/otel/package-cjs.json +++ b/js/examples/otel/package-cjs.json @@ -6,7 +6,7 @@ "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK (CommonJS)", "main": "nodesdk_example.ts", "scripts": { - "start": "TS_NODE_TRANSPILE_ONLY=true node -r ts-node/register nodesdk_example.ts" + "start": "tsx nodesdk_example.ts" }, "dependencies": { "@traceloop/instrumentation-openai": "^0.14.0", @@ -19,10 +19,10 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "openai": "^4.73.1", - "ts-node": "^10.9.2" + "tsx": "^4.19.2" }, "devDependencies": { - "@types/node": "^20.10.5", + "@types/node": "^14.18.0", "typescript": "^5.4.4" } } diff --git a/js/integration-tests/distributed-tracing.test.ts b/js/integration-tests/cjs/distributed-tracing.test.ts similarity index 100% rename from js/integration-tests/distributed-tracing.test.ts rename to js/integration-tests/cjs/distributed-tracing.test.ts diff --git a/js/integration-tests/nodesdk.test.ts b/js/integration-tests/esm/nodesdk.test.ts similarity index 100% rename from js/integration-tests/nodesdk.test.ts rename to js/integration-tests/esm/nodesdk.test.ts From 2b1505b957dc333c8d11e7b8bb2865c94a27ee7d Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 15:11:28 -0800 Subject: [PATCH 08/23] build with node 20 test with 14 --- .github/workflows/js-sdk-node-example.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index 8222b08d4..f15744c92 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -18,7 +18,20 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - - name: Setup Node.js + - name: Setup Node.js 20 for building + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install SDK dependencies + working-directory: js + run: pnpm install + + - name: Build SDK + working-directory: js + run: npm run build + + - name: Setup Node.js 14 for testing uses: actions/setup-node@v4 with: node-version: "14" From a7e9eb354362ebcf45bc5a0b040b751ca148683d Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 15:15:30 -0800 Subject: [PATCH 09/23] node 14 commonjs --- .github/workflows/js-sdk-node-example.yaml | 17 +++++++++++++++-- js/examples/otel/package-cjs.json | 8 ++++---- js/examples/otel/tsconfig-cjs.json | 12 +++++------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index f15744c92..b2c596d75 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -57,6 +57,19 @@ jobs: working-directory: js/examples/otel run: npm install + - name: Compile example to JavaScript + working-directory: js/examples/otel + run: | + npm run build + echo "Verifying compiled file exists..." + ls -la nodesdk_example.js + echo "Compiled successfully!" + + - name: Setup Node.js 14 for testing + uses: actions/setup-node@v4 + with: + node-version: "14" + - name: Verify OTEL dependencies installed working-directory: js/examples/otel run: | @@ -67,7 +80,6 @@ jobs: "@opentelemetry/sdk-node" "@opentelemetry/context-async-hooks" "openai" - "tsx" ) for package in "${packages[@]}"; do if [ ! -d "node_modules/$package" ]; then @@ -105,5 +117,6 @@ jobs: if [ -f tsconfig-esm.json.bak ]; then mv tsconfig-esm.json.bak tsconfig.json || true fi - # Restore original imports + # Restore original imports and remove compiled file git checkout nodesdk_example.ts || true + rm -f nodesdk_example.js || true diff --git a/js/examples/otel/package-cjs.json b/js/examples/otel/package-cjs.json index af0931140..48e9181b8 100644 --- a/js/examples/otel/package-cjs.json +++ b/js/examples/otel/package-cjs.json @@ -4,9 +4,10 @@ "version": "1.0.0", "type": "commonjs", "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK (CommonJS)", - "main": "nodesdk_example.ts", + "main": "nodesdk_example.js", "scripts": { - "start": "tsx nodesdk_example.ts" + "build": "tsc --project tsconfig.json --outDir .", + "start": "node nodesdk_example.js" }, "dependencies": { "@traceloop/instrumentation-openai": "^0.14.0", @@ -18,8 +19,7 @@ "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", - "openai": "^4.73.1", - "tsx": "^4.19.2" + "openai": "^4.73.1" }, "devDependencies": { "@types/node": "^14.18.0", diff --git a/js/examples/otel/tsconfig-cjs.json b/js/examples/otel/tsconfig-cjs.json index 05bb40b20..feac29ea7 100644 --- a/js/examples/otel/tsconfig-cjs.json +++ b/js/examples/otel/tsconfig-cjs.json @@ -8,12 +8,10 @@ "esModuleInterop": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "declaration": false, + "sourceMap": false }, - "ts-node": { - "transpileOnly": true, - "compilerOptions": { - "module": "CommonJS" - } - } + "include": ["nodesdk_example.ts"], + "exclude": ["node_modules"] } From 1de20d9e03858807cb75a699b75688ce220beef7 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 15:21:48 -0800 Subject: [PATCH 10/23] attempt to ignroe tsx errors --- .github/workflows/js-sdk-node-example.yaml | 12 +++-------- js/examples/otel/distributed-tracing.ts | 2 +- js/examples/otel/nodesdk_example.ts | 2 +- js/examples/otel/package-cjs.json | 20 ++++++++++--------- js/examples/otel/tsconfig-cjs.json | 7 +++++-- .../cjs/distributed-tracing.test.ts | 6 ++++-- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index b2c596d75..d1593aba1 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -49,9 +49,8 @@ jobs: cp tsconfig-cjs.json tsconfig.json # Update import to use compiled CJS dist files - sed -i 's|from "../../src"|from "../../dist/index.js"|g' nodesdk_example.ts - echo "Updated nodesdk_example.ts to use compiled CJS files" - cat nodesdk_example.ts | grep "import.*BraintrustSpanProcessor" + sed -i 's|from "../../src"|from "../../dist/index.js"|g' distributed-tracing.ts + echo "Updated distributed-tracing.ts to use compiled CJS files" - name: Install OTEL example dependencies working-directory: js/examples/otel @@ -62,14 +61,9 @@ jobs: run: | npm run build echo "Verifying compiled file exists..." - ls -la nodesdk_example.js + ls -la distributed-tracing.js echo "Compiled successfully!" - - name: Setup Node.js 14 for testing - uses: actions/setup-node@v4 - with: - node-version: "14" - - name: Verify OTEL dependencies installed working-directory: js/examples/otel run: | diff --git a/js/examples/otel/distributed-tracing.ts b/js/examples/otel/distributed-tracing.ts index 8bcbdaad0..de64370d4 100755 --- a/js/examples/otel/distributed-tracing.ts +++ b/js/examples/otel/distributed-tracing.ts @@ -14,7 +14,7 @@ import { otel, BraintrustSpanProcessor, BraintrustExporter, -} from "../../dist/index.js"; +} from "braintrust"; const { trace, context, propagation } = api; diff --git a/js/examples/otel/nodesdk_example.ts b/js/examples/otel/nodesdk_example.ts index 63ae96274..21a51e117 100644 --- a/js/examples/otel/nodesdk_example.ts +++ b/js/examples/otel/nodesdk_example.ts @@ -1,6 +1,6 @@ // nodesdk_example.ts import { NodeSDK } from "@opentelemetry/sdk-node"; -import { BraintrustSpanProcessor } from "../../src"; +import { BraintrustSpanProcessor } from "braintrust"; import { trace } from "@opentelemetry/api"; const sdk = new NodeSDK({ diff --git a/js/examples/otel/package-cjs.json b/js/examples/otel/package-cjs.json index 48e9181b8..39b1df1e5 100644 --- a/js/examples/otel/package-cjs.json +++ b/js/examples/otel/package-cjs.json @@ -4,22 +4,24 @@ "version": "1.0.0", "type": "commonjs", "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK (CommonJS)", - "main": "nodesdk_example.js", + "main": "distributed-tracing.js", "scripts": { - "build": "tsc --project tsconfig.json --outDir .", - "start": "node nodesdk_example.js" + "build": "tsc --project tsconfig.json --outDir . || exit 0", + "start": "node distributed-tracing.js" }, + "dependencies": { "@traceloop/instrumentation-openai": "^0.14.0", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-base": "^1.28.0", + "@opentelemetry/sdk-node": "^0.54.2", + "@opentelemetry/sdk-trace-node": "^1.28.0", + "@opentelemetry/resources": "^1.28.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.54.2", "@opentelemetry/auto-instrumentations-node": "^0.62.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", - "@opentelemetry/resources": "^2.0.1", - "@opentelemetry/sdk-node": "^0.203.0", - "@opentelemetry/sdk-trace-base": "^2.0.1", - "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", - "openai": "^4.73.1" + "openai": "^4.73.1", + "braintrust": "latest" }, "devDependencies": { "@types/node": "^14.18.0", diff --git a/js/examples/otel/tsconfig-cjs.json b/js/examples/otel/tsconfig-cjs.json index feac29ea7..ec9318892 100644 --- a/js/examples/otel/tsconfig-cjs.json +++ b/js/examples/otel/tsconfig-cjs.json @@ -10,8 +10,11 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "declaration": false, - "sourceMap": false + "sourceMap": false, + "noEmitOnError": false, + "strict": false, + "noImplicitAny": false }, - "include": ["nodesdk_example.ts"], + "include": ["distributed-tracing.ts"], "exclude": ["node_modules"] } diff --git a/js/integration-tests/cjs/distributed-tracing.test.ts b/js/integration-tests/cjs/distributed-tracing.test.ts index 2006d7251..369ba2bfd 100644 --- a/js/integration-tests/cjs/distributed-tracing.test.ts +++ b/js/integration-tests/cjs/distributed-tracing.test.ts @@ -10,13 +10,15 @@ import { import * as api from "@opentelemetry/api"; import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; -import { +import braintrust from "../../dist/index.js"; + +const { initLogger, login, otel, BraintrustSpanProcessor, _exportsForTestingOnly, -} from "braintrust"; +} = braintrust; const { trace, context, propagation } = api; From 5e64e4a81bc42d1929efb9458d351d775bf7804e Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 16:33:21 -0800 Subject: [PATCH 11/23] transpile to node 14 --- .github/workflows/js-sdk-node-example.yaml | 6 +++--- js/examples/otel/distributed-tracing.ts | 2 +- js/examples/otel/package-cjs.json | 23 +++++++++++----------- js/examples/otel/package.json | 3 ++- js/examples/otel/tsconfig-cjs.json | 2 +- js/examples/otel/tsup.config.ts | 14 +++++++++++++ 6 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 js/examples/otel/tsup.config.ts diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index d1593aba1..50a80ebc0 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -49,8 +49,8 @@ jobs: cp tsconfig-cjs.json tsconfig.json # Update import to use compiled CJS dist files - sed -i 's|from "../../src"|from "../../dist/index.js"|g' distributed-tracing.ts - echo "Updated distributed-tracing.ts to use compiled CJS files" + sed -i 's|from "../../src"|from "../../dist/index.js"|g' nodesdk_example.ts + echo "Updated nodesdk_example.ts to use compiled CJS files" - name: Install OTEL example dependencies working-directory: js/examples/otel @@ -61,7 +61,7 @@ jobs: run: | npm run build echo "Verifying compiled file exists..." - ls -la distributed-tracing.js + ls -la nodesdk_example.js echo "Compiled successfully!" - name: Verify OTEL dependencies installed diff --git a/js/examples/otel/distributed-tracing.ts b/js/examples/otel/distributed-tracing.ts index de64370d4..f4179daaa 100755 --- a/js/examples/otel/distributed-tracing.ts +++ b/js/examples/otel/distributed-tracing.ts @@ -55,7 +55,7 @@ async function main() { async (spanC) => { spanC.log({ input: "from service B" }); }, - { name: "service_c", parent }, + { name: "service_c" }, ); spanB.end(); diff --git a/js/examples/otel/package-cjs.json b/js/examples/otel/package-cjs.json index 39b1df1e5..3b24849ca 100644 --- a/js/examples/otel/package-cjs.json +++ b/js/examples/otel/package-cjs.json @@ -4,27 +4,26 @@ "version": "1.0.0", "type": "commonjs", "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK (CommonJS)", - "main": "distributed-tracing.js", + "main": "nodesdk_example.js", "scripts": { - "build": "tsc --project tsconfig.json --outDir . || exit 0", - "start": "node distributed-tracing.js" + "build": "npx tsup --config tsup.config.ts && (tsc --project tsconfig.json --outDir . || exit 0)", + "start": "node dist/nodesdk_example.js" }, - "dependencies": { - "@traceloop/instrumentation-openai": "^0.14.0", + "openai": "^4.73.1", + "braintrust": "latest", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.28.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.54.2", + "@opentelemetry/resources": "^1.28.0", "@opentelemetry/sdk-trace-base": "^1.28.0", - "@opentelemetry/sdk-node": "^0.54.2", "@opentelemetry/sdk-trace-node": "^1.28.0", - "@opentelemetry/resources": "^1.28.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.54.2", - "@opentelemetry/auto-instrumentations-node": "^0.62.0", - "@opentelemetry/semantic-conventions": "^1.36.0", - "openai": "^4.73.1", - "braintrust": "latest" + "@opentelemetry/semantic-conventions": "^1.28.0", + "@opentelemetry/sdk-node": "^0.54.2" }, "devDependencies": { "@types/node": "^14.18.0", + "tsup": "^6.7.0", "typescript": "^5.4.4" } } diff --git a/js/examples/otel/package.json b/js/examples/otel/package.json index f1c71026c..27323a930 100644 --- a/js/examples/otel/package.json +++ b/js/examples/otel/package.json @@ -17,7 +17,8 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "openai": "^4.73.1", - "tsx": "^4.19.2" + "tsx": "^4.19.2", + "braintrust": "latest" }, "devDependencies": { "@types/node": "^20.10.5", diff --git a/js/examples/otel/tsconfig-cjs.json b/js/examples/otel/tsconfig-cjs.json index ec9318892..6e80cea5f 100644 --- a/js/examples/otel/tsconfig-cjs.json +++ b/js/examples/otel/tsconfig-cjs.json @@ -15,6 +15,6 @@ "strict": false, "noImplicitAny": false }, - "include": ["distributed-tracing.ts"], + "include": ["nodesdk_example.ts"], "exclude": ["node_modules"] } diff --git a/js/examples/otel/tsup.config.ts b/js/examples/otel/tsup.config.ts new file mode 100644 index 000000000..826c7bb76 --- /dev/null +++ b/js/examples/otel/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["nodesdk_example.ts"], + format: ["cjs", "esm"], + target: "node14", + outDir: "dist", + clean: true, + splitting: false, + sourcemap: false, + minify: false, + dts: false, + noExternal: [/.*/], +}); From 736d4918238e80d1ff0d7a25b87bd133c2849e3a Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 19:37:47 -0800 Subject: [PATCH 12/23] workflow action to upload and then download file --- .github/workflows/js-sdk-integration.yaml | 79 +++++++++++++++++++ .github/workflows/js.yaml | 6 ++ js/examples/otel/package.json | 3 +- .../cjs/distributed-tracing.test.ts | 5 +- 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/js-sdk-integration.yaml diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml new file mode 100644 index 000000000..ea1277ee5 --- /dev/null +++ b/.github/workflows/js-sdk-integration.yaml @@ -0,0 +1,79 @@ +name: js-sdk-integration + +on: + workflow_run: + workflows: ["js"] + types: + - completed + +permissions: + contents: read + +jobs: + run-nodesdk-example: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["20", "22"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.repository.full_name }} + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Determine artifact name + id: artifact + env: + PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number || '' }} + NODE_VERSION: ${{ matrix.node-version }} + run: | + if [ -z "$PR_NUMBER" ]; then + ARTIFACT_NAME="javascript-sdk-dev--${NODE_VERSION}-dist" + else + ARTIFACT_NAME="javascript-sdk-dev-${PR_NUMBER}-${NODE_VERSION}-dist" + fi + echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" + + - name: Download javascript dist artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.name }} + path: sdk/js/artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install example dependencies + working-directory: sdk/js/examples/otel + run: npm install + + - name: Replace braintrust dist with build artifact + working-directory: sdk/js/examples/otel + env: + ARTIFACT_NAME: ${{ steps.artifact.outputs.name }} + run: | + DIST_SRC="../artifacts/${ARTIFACT_NAME}/js/dist" + if [ ! -d "$DIST_SRC" ]; then + echo "Expected dist directory not found at $DIST_SRC" >&2 + echo "Available files:" >&2 + find ../artifacts -maxdepth 3 -type d >&2 + exit 1 + fi + rm -rf node_modules/braintrust/dist + mkdir -p node_modules/braintrust + cp -a "$DIST_SRC" node_modules/braintrust/ + + - name: Run NodeSDK example + working-directory: sdk/js/examples/otel + env: + BRAINTRUST_OTEL_COMPAT: true + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + run: npm run sdk-example diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 2260740c0..30ba3c19f 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -30,3 +30,9 @@ jobs: - name: "verify ci" shell: bash run: make js-verify-ci + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: javascript-sdk-dev-${{ github.event.number }}-${{ matrix.node-version }}-dist + path: js/dist/ + retention-days: 1 diff --git a/js/examples/otel/package.json b/js/examples/otel/package.json index 27323a930..c59b9ab8a 100644 --- a/js/examples/otel/package.json +++ b/js/examples/otel/package.json @@ -4,7 +4,8 @@ "description": "Example using BraintrustSpanProcessor with OpenTelemetry NodeSDK", "main": "nodesdk_example.ts", "scripts": { - "start": "tsx nodesdk_example.ts" + "start": "tsx nodesdk_example.ts", + "sdk-example": "tsx nodesdk_example.ts" }, "dependencies": { "@traceloop/instrumentation-openai": "^0.14.0", diff --git a/js/integration-tests/cjs/distributed-tracing.test.ts b/js/integration-tests/cjs/distributed-tracing.test.ts index 369ba2bfd..2909a0a22 100644 --- a/js/integration-tests/cjs/distributed-tracing.test.ts +++ b/js/integration-tests/cjs/distributed-tracing.test.ts @@ -10,15 +10,14 @@ import { import * as api from "@opentelemetry/api"; import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; -import braintrust from "../../dist/index.js"; -const { +import { initLogger, login, otel, BraintrustSpanProcessor, _exportsForTestingOnly, -} = braintrust; +} from "braintrust"; const { trace, context, propagation } = api; From 621c380e81522c85a974d651dc324ec05201c3af Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 19:47:27 -0800 Subject: [PATCH 13/23] remove tests --- .github/workflows/js-sdk-integration.yaml | 15 +- .github/workflows/js.yaml | 1 + .../cjs/distributed-tracing.test.ts | 223 ------------ js/integration-tests/esm/nodesdk.test.ts | 339 ------------------ js/integration-tests/package-cjs.json | 25 -- js/integration-tests/package.json | 27 -- js/integration-tests/tsconfig-cjs.json | 13 - js/integration-tests/tsconfig.json | 12 - js/integration-tests/vitest.config.ts | 18 - 9 files changed, 4 insertions(+), 669 deletions(-) delete mode 100644 js/integration-tests/cjs/distributed-tracing.test.ts delete mode 100644 js/integration-tests/esm/nodesdk.test.ts delete mode 100644 js/integration-tests/package-cjs.json delete mode 100644 js/integration-tests/package.json delete mode 100644 js/integration-tests/tsconfig-cjs.json delete mode 100644 js/integration-tests/tsconfig.json delete mode 100644 js/integration-tests/vitest.config.ts diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml index ea1277ee5..6ba6ec0f0 100644 --- a/.github/workflows/js-sdk-integration.yaml +++ b/.github/workflows/js-sdk-integration.yaml @@ -1,6 +1,7 @@ name: js-sdk-integration on: + workflow_dispatch: workflow_run: workflows: ["js"] types: @@ -10,8 +11,7 @@ permissions: contents: read jobs: - run-nodesdk-example: - if: ${{ github.event.workflow_run.conclusion == 'success' }} + test-node-sdk-example: runs-on: ubuntu-latest strategy: fail-fast: false @@ -60,16 +60,7 @@ jobs: env: ARTIFACT_NAME: ${{ steps.artifact.outputs.name }} run: | - DIST_SRC="../artifacts/${ARTIFACT_NAME}/js/dist" - if [ ! -d "$DIST_SRC" ]; then - echo "Expected dist directory not found at $DIST_SRC" >&2 - echo "Available files:" >&2 - find ../artifacts -maxdepth 3 -type d >&2 - exit 1 - fi - rm -rf node_modules/braintrust/dist - mkdir -p node_modules/braintrust - cp -a "$DIST_SRC" node_modules/braintrust/ + npm install "file:../../artifacts/${{ env.ARTIFACT_NAME }}" - name: Run NodeSDK example working-directory: sdk/js/examples/otel diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 30ba3c19f..292cfd3cc 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -31,6 +31,7 @@ jobs: shell: bash run: make js-verify-ci - name: Upload build artifacts + if: matrix.node-version == '22' && matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: name: javascript-sdk-dev-${{ github.event.number }}-${{ matrix.node-version }}-dist diff --git a/js/integration-tests/cjs/distributed-tracing.test.ts b/js/integration-tests/cjs/distributed-tracing.test.ts deleted file mode 100644 index 2909a0a22..000000000 --- a/js/integration-tests/cjs/distributed-tracing.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - describe, - it, - expect, - beforeAll, - afterAll, - beforeEach, - afterEach, -} from "vitest"; -import * as api from "@opentelemetry/api"; -import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; -import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; - -import { - initLogger, - login, - otel, - BraintrustSpanProcessor, - _exportsForTestingOnly, -} from "braintrust"; - -const { trace, context, propagation } = api; - -describe("Distributed Tracing: BT → OTEL → BT", () => { - let provider: BasicTracerProvider; - let tracer: api.Tracer; - let contextManager: AsyncHooksContextManager; - let logger: ReturnType; - let testLogger: ReturnType< - typeof _exportsForTestingOnly.useTestBackgroundLogger - >; - - beforeAll(async () => { - process.env.BRAINTRUST_OTEL_COMPAT = "true"; - - provider = new BasicTracerProvider({ - spanProcessors: [new BraintrustSpanProcessor()], - }); - trace.setGlobalTracerProvider(provider); - tracer = trace.getTracer("service-b"); - - contextManager = new AsyncHooksContextManager(); - contextManager.enable(); - context.setGlobalContextManager(contextManager); - - await _exportsForTestingOnly.simulateLoginForTests(); - logger = initLogger({ - projectName: "otel-integration-tests", - projectId: "test-project-id", - }); - }); - - beforeEach(() => { - testLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - }); - - afterEach(() => { - _exportsForTestingOnly.clearTestBackgroundLogger(); - }); - - afterAll(async () => { - await logger.flush(); - await provider.forceFlush(); - contextManager.disable(); - }); - - it("should successfully trace across BT → OTEL → BT services", async () => { - let spanLink = ""; - let spanCId: string | undefined; - - await logger.traced(async (spanA) => { - spanLink = spanA.link(); - const exported = await spanA.export(); - - expect(exported).toBeDefined(); - expect(typeof exported).toBe("string"); - - const ctx = otel.contextFromSpanExport(exported); - await context.with(ctx, async () => { - await tracer.startActiveSpan("service_b", async (spanB: api.Span) => { - const currentCtx = otel.addSpanParentToBaggage(spanB); - const headers: Record = {}; - propagation.inject(currentCtx, headers); - - expect(headers).toBeDefined(); - expect(Object.keys(headers).length).toBeGreaterThan(0); - - const parent = otel.parentFromHeaders(headers); - await logger.traced( - async (spanC) => { - spanC.log({ input: { message: "from service B" } }); - spanCId = spanC.id; - }, - { name: "service_c", parent }, - ); - - spanB.end(); - }); - }); - }); - - expect(spanLink).toBeTruthy(); - expect(spanCId).toBeDefined(); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(2); - - const spanNames = spans.map((s: any) => s.span_attributes?.name); - expect(spanNames).toContain("service_c"); - - const serviceCSpan = spans.find( - (s: any) => s.span_attributes?.name === "service_c", - ); - expect(serviceCSpan).toBeDefined(); - expect(serviceCSpan.input).toEqual({ message: "from service B" }); - }, 30000); - - it("should propagate context correctly through OTEL middleware", async () => { - const results: string[] = []; - - await logger.traced(async (spanA) => { - results.push("spanA"); - spanA.log({ input: { service: "A" } }); - - const exported = await spanA.export(); - const ctx = otel.contextFromSpanExport(exported); - - await context.with(ctx, async () => { - await tracer.startActiveSpan( - "service_b_operation", - async (spanB: api.Span) => { - results.push("spanB"); - spanB.setAttribute("operation", "process"); - - const currentCtx = otel.addSpanParentToBaggage(spanB); - const headers: Record = {}; - propagation.inject(currentCtx, headers); - - const parent = otel.parentFromHeaders(headers); - await logger.traced( - async (spanC) => { - results.push("spanC"); - spanC.log({ output: { result: "processed" } }); - }, - { name: "service_c_operation", parent }, - ); - - spanB.end(); - }, - ); - }); - }); - - expect(results).toEqual(["spanA", "spanB", "spanC"]); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(2); - - const serviceASpan = spans.find((s: any) => s.input?.service === "A"); - expect(serviceASpan).toBeDefined(); - - const serviceCSpan = spans.find( - (s: any) => s.span_attributes?.name === "service_c_operation", - ); - expect(serviceCSpan).toBeDefined(); - expect(serviceCSpan.output).toEqual({ result: "processed" }); - }, 30000); - - it("should handle multiple parallel OTEL operations", async () => { - const operations = ["op1", "op2", "op3"]; - const completedOps: string[] = []; - - await logger.traced(async (spanA) => { - const exported = await spanA.export(); - const ctx = otel.contextFromSpanExport(exported); - - await Promise.all( - operations.map((op) => - context.with(ctx, async () => { - await tracer.startActiveSpan( - `service_b_${op}`, - async (spanB: api.Span) => { - const currentCtx = otel.addSpanParentToBaggage(spanB); - const headers: Record = {}; - propagation.inject(currentCtx, headers); - - const parent = otel.parentFromHeaders(headers); - await logger.traced( - async (spanC) => { - spanC.log({ input: { operation: op } }); - completedOps.push(op); - }, - { name: `service_c_${op}`, parent }, - ); - - spanB.end(); - }, - ); - }), - ), - ); - }); - - expect(completedOps.sort()).toEqual(operations.sort()); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(3); - - const serviceCSpans = spans.filter((s: any) => - s.span_attributes?.name?.startsWith("service_c_"), - ); - expect(serviceCSpans.length).toBe(3); - - const loggedOps = serviceCSpans.map((s: any) => s.input?.operation).sort(); - expect(loggedOps).toEqual(operations.sort()); - }, 30000); -}); diff --git a/js/integration-tests/esm/nodesdk.test.ts b/js/integration-tests/esm/nodesdk.test.ts deleted file mode 100644 index 13269c2c8..000000000 --- a/js/integration-tests/esm/nodesdk.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { - describe, - it, - expect, - beforeAll, - afterAll, - beforeEach, - afterEach, -} from "vitest"; -import { NodeSDK } from "@opentelemetry/sdk-node"; -import { BraintrustSpanProcessor, _exportsForTestingOnly } from "braintrust"; -import { trace } from "@opentelemetry/api"; - -describe("NodeSDK with BraintrustSpanProcessor", () => { - let sdk: NodeSDK; - let testLogger: ReturnType< - typeof _exportsForTestingOnly.useTestBackgroundLogger - >; - - beforeAll(async () => { - await _exportsForTestingOnly.simulateLoginForTests(); - - sdk = new NodeSDK({ - serviceName: "integration-test-service", - spanProcessor: new BraintrustSpanProcessor({ - parent: "project_name:otel-integration-tests", - filterAISpans: true, - }), - }); - - sdk.start(); - }); - - beforeEach(() => { - testLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - }); - - afterEach(() => { - _exportsForTestingOnly.clearTestBackgroundLogger(); - }); - - afterAll(async () => { - await sdk.shutdown(); - }); - - it("should initialize NodeSDK with BraintrustSpanProcessor", () => { - expect(sdk).toBeDefined(); - const tracer = trace.getTracer("test-service", "1.0.0"); - expect(tracer).toBeDefined(); - }); - - it("should create and export root spans", async () => { - const tracer = trace.getTracer("test-service", "1.0.0"); - - await tracer.startActiveSpan("test_root_span", async (rootSpan) => { - rootSpan.setAttributes({ - "test.type": "integration", - "test.timestamp": new Date().toISOString(), - }); - - expect(rootSpan.spanContext().traceId).toBeTruthy(); - expect(rootSpan.spanContext().spanId).toBeTruthy(); - - rootSpan.end(); - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(1); - - const rootSpan = spans.find( - (s: any) => s.span_attributes?.name === "test_root_span", - ); - expect(rootSpan).toBeDefined(); - expect(rootSpan.metadata?.["test.type"]).toBe("integration"); - }, 15000); - - it("should filter AI spans when filterAISpans is enabled", async () => { - const tracer = trace.getTracer("test-service", "1.0.0"); - - await tracer.startActiveSpan("test_filter_ai", async (rootSpan) => { - rootSpan.setAttributes({ - "test.id": "filter_test", - }); - - await tracer.startActiveSpan( - "gen_ai.chat.completions", - async (aiSpan) => { - aiSpan.setAttributes({ - "gen_ai.system": "openai", - "gen_ai.operation.name": "chat.completions", - "gen_ai.request.model": "gpt-4", - }); - - expect(aiSpan.spanContext().spanId).toBeTruthy(); - aiSpan.end(); - }, - ); - - await tracer.startActiveSpan("non_ai_operation", async (nonAiSpan) => { - nonAiSpan.setAttributes({ - "operation.type": "database", - }); - - expect(nonAiSpan.spanContext().spanId).toBeTruthy(); - nonAiSpan.end(); - }); - - rootSpan.end(); - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(2); - - const spanNames = spans.map((s: any) => s.span_attributes?.name); - expect(spanNames).toContain("test_filter_ai"); - expect(spanNames).toContain("gen_ai.chat.completions"); - - const aiSpan = spans.find( - (s: any) => s.span_attributes?.name === "gen_ai.chat.completions", - ); - expect(aiSpan).toBeDefined(); - expect(aiSpan.metadata?.["gen_ai.system"]).toBe("openai"); - }, 15000); - - it("should handle nested spans correctly", async () => { - const tracer = trace.getTracer("test-service", "1.0.0"); - const spanIds: string[] = []; - - await tracer.startActiveSpan("parent_operation", async (parentSpan) => { - spanIds.push(parentSpan.spanContext().spanId); - - parentSpan.setAttributes({ - operation: "parent", - level: 0, - }); - - await tracer.startActiveSpan("child_operation_1", async (child1) => { - spanIds.push(child1.spanContext().spanId); - child1.setAttributes({ - operation: "child1", - level: 1, - }); - - await tracer.startActiveSpan( - "grandchild_operation", - async (grandchild) => { - spanIds.push(grandchild.spanContext().spanId); - grandchild.setAttributes({ - operation: "grandchild", - level: 2, - }); - grandchild.end(); - }, - ); - - child1.end(); - }); - - await tracer.startActiveSpan("child_operation_2", async (child2) => { - spanIds.push(child2.spanContext().spanId); - child2.setAttributes({ - operation: "child2", - level: 1, - }); - child2.end(); - }); - - parentSpan.end(); - }); - - expect(spanIds).toHaveLength(4); - expect(new Set(spanIds).size).toBe(4); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(4); - - const parentSpan = spans.find( - (s: any) => s.metadata?.operation === "parent", - ); - const child1Span = spans.find( - (s: any) => s.metadata?.operation === "child1", - ); - const grandchildSpan = spans.find( - (s: any) => s.metadata?.operation === "grandchild", - ); - const child2Span = spans.find( - (s: any) => s.metadata?.operation === "child2", - ); - - expect(parentSpan).toBeDefined(); - expect(child1Span).toBeDefined(); - expect(grandchildSpan).toBeDefined(); - expect(child2Span).toBeDefined(); - - expect(parentSpan.metadata?.level).toBe(0); - expect(child1Span.metadata?.level).toBe(1); - expect(grandchildSpan.metadata?.level).toBe(2); - expect(child2Span.metadata?.level).toBe(1); - }, 15000); - - it("should capture span attributes correctly", async () => { - const tracer = trace.getTracer("test-service", "1.0.0"); - - await tracer.startActiveSpan("attribute_test", async (span) => { - const testAttributes = { - "string.attr": "test_value", - "number.attr": 42, - "boolean.attr": true, - "timestamp.attr": new Date().toISOString(), - }; - - span.setAttributes(testAttributes); - - expect(span.spanContext().traceId).toBeTruthy(); - - span.end(); - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(1); - - const attrSpan = spans.find( - (s: any) => s.span_attributes?.name === "attribute_test", - ); - expect(attrSpan).toBeDefined(); - expect(attrSpan.metadata?.["string.attr"]).toBe("test_value"); - expect(attrSpan.metadata?.["number.attr"]).toBe(42); - expect(attrSpan.metadata?.["boolean.attr"]).toBe(true); - }, 15000); - - it("should handle AI-related span prefixes", async () => { - const tracer = trace.getTracer("test-service", "1.0.0"); - const aiPrefixes = [ - "gen_ai.completion", - "llm.generate", - "ai.model_call", - "braintrust.eval", - "traceloop.workflow", - ]; - - await tracer.startActiveSpan("ai_prefix_test_root", async (rootSpan) => { - for (const prefix of aiPrefixes) { - await tracer.startActiveSpan(prefix, async (aiSpan) => { - aiSpan.setAttributes({ - "test.prefix": prefix, - "test.type": "ai_span", - }); - - expect(aiSpan.spanContext().spanId).toBeTruthy(); - aiSpan.end(); - }); - } - - rootSpan.end(); - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(6); - - const rootSpan = spans.find( - (s: any) => s.span_attributes?.name === "ai_prefix_test_root", - ); - expect(rootSpan).toBeDefined(); - - for (const prefix of aiPrefixes) { - const aiSpan = spans.find((s: any) => s.span_attributes?.name === prefix); - expect(aiSpan).toBeDefined(); - expect(aiSpan.metadata?.["test.prefix"]).toBe(prefix); - expect(aiSpan.metadata?.["test.type"]).toBe("ai_span"); - } - }, 15000); - - it("should support parallel span creation", async () => { - const tracer = trace.getTracer("test-service", "1.0.0"); - - await tracer.startActiveSpan("parallel_test_root", async (rootSpan) => { - const operations = Array.from({ length: 5 }, (_, i) => i); - - await Promise.all( - operations.map(async (i) => { - await tracer.startActiveSpan(`parallel_op_${i}`, async (span) => { - span.setAttributes({ - "operation.id": i, - "operation.type": "parallel", - }); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - span.end(); - }); - }), - ); - - rootSpan.end(); - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await testLogger.flush(); - const spans = await testLogger.drain(); - - expect(spans.length).toBeGreaterThanOrEqual(6); - - const rootSpan = spans.find( - (s: any) => s.span_attributes?.name === "parallel_test_root", - ); - expect(rootSpan).toBeDefined(); - - const parallelSpans = spans.filter((s: any) => - s.span_attributes?.name?.startsWith("parallel_op_"), - ); - expect(parallelSpans.length).toBe(5); - - parallelSpans.forEach((span: any) => { - expect(span.metadata?.["operation.type"]).toBe("parallel"); - expect(typeof span.metadata?.["operation.id"]).toBe("number"); - }); - }, 15000); -}); diff --git a/js/integration-tests/package-cjs.json b/js/integration-tests/package-cjs.json deleted file mode 100644 index 35d8674a3..000000000 --- a/js/integration-tests/package-cjs.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$comment": "This is the CommonJS configuration used by .github/workflows/js-sdk-cjs.yaml - DO NOT remove 'type' field", - "name": "braintrust-otel-integration-tests", - "version": "1.0.0", - "description": "Integration tests for Braintrust OpenTelemetry integration (CommonJS)", - "scripts": { - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.0.1", - "@opentelemetry/core": "^2.0.1", - "@opentelemetry/sdk-node": "^0.203.0", - "@opentelemetry/sdk-trace-base": "^2.0.1", - "@opentelemetry/sdk-trace-node": "^2.0.1", - "braintrust": "workspace:*" - }, - "devDependencies": { - "@types/node": "^20.10.5", - "typescript": "^5.4.4", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^2.1.9" - } -} diff --git a/js/integration-tests/package.json b/js/integration-tests/package.json deleted file mode 100644 index 3d542e786..000000000 --- a/js/integration-tests/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "braintrust-otel-integration-tests", - "version": "1.0.0", - "description": "Integration tests for Braintrust OpenTelemetry integration", - "type": "module", - "scripts": { - "test": "vitest run", - "test:watch": "vitest", - "start:node": "node nodesdk.test.ts", - "start:otel": "node otel.test.ts" - }, - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.0.1", - "@opentelemetry/core": "^2.0.1", - "@opentelemetry/sdk-node": "^0.203.0", - "@opentelemetry/sdk-trace-base": "^2.0.1", - "@opentelemetry/sdk-trace-node": "^2.0.1", - "braintrust": "workspace:*" - }, - "devDependencies": { - "@types/node": "^20.10.5", - "typescript": "^5.4.4", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^2.1.9" - } -} diff --git a/js/integration-tests/tsconfig-cjs.json b/js/integration-tests/tsconfig-cjs.json deleted file mode 100644 index 37bcf8421..000000000 --- a/js/integration-tests/tsconfig-cjs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$comment": "This is the CommonJS tsconfig used by .github/workflows/js-sdk-cjs.yaml", - "extends": "../tsconfig.json", - "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "node", - "target": "ES2022", - "lib": ["ES2022"], - "types": ["node", "vitest/globals"] - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/js/integration-tests/tsconfig.json b/js/integration-tests/tsconfig.json deleted file mode 100644 index d120075dc..000000000 --- a/js/integration-tests/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", - "target": "ES2022", - "lib": ["ES2022"], - "types": ["node", "vitest/globals"] - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/js/integration-tests/vitest.config.ts b/js/integration-tests/vitest.config.ts deleted file mode 100644 index b773ee8ef..000000000 --- a/js/integration-tests/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "vitest/config"; -import tsconfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - plugins: [tsconfigPaths()], - test: { - environment: "node", - testTimeout: 30000, - hookTimeout: 30000, - globals: false, - include: ["**/*.test.ts"], - exclude: [ - "**/node_modules/**", - "**/dist/**", - "**/.{idea,git,cache,output,temp}/**", - ], - }, -}); From b958ef60648f5ed32fdeb24932cf1c71d19befd3 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 20:25:41 -0800 Subject: [PATCH 14/23] test workflow --- .github/workflows/js-sdk-integration.yaml | 24 +++++++++-------------- .github/workflows/js.yaml | 12 +++++++++++- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml index 6ba6ec0f0..b3028ff9b 100644 --- a/.github/workflows/js-sdk-integration.yaml +++ b/.github/workflows/js-sdk-integration.yaml @@ -2,21 +2,19 @@ name: js-sdk-integration on: workflow_dispatch: - workflow_run: - workflows: ["js"] - types: - - completed + inputs: + build_run_id: + description: "The run ID of the js workflow build" + required: true + type: string permissions: contents: read + actions: read jobs: test-node-sdk-example: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - node-version: ["20", "22"] steps: - name: Checkout repository @@ -28,14 +26,10 @@ jobs: - name: Determine artifact name id: artifact env: - PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number || '' }} NODE_VERSION: ${{ matrix.node-version }} + RUN_ID: ${{ github.event.workflow_run.id }} run: | - if [ -z "$PR_NUMBER" ]; then - ARTIFACT_NAME="javascript-sdk-dev--${NODE_VERSION}-dist" - else - ARTIFACT_NAME="javascript-sdk-dev-${PR_NUMBER}-${NODE_VERSION}-dist" - fi + ARTIFACT_NAME="javascript-sdk-dev-${RUN_ID}-22-dist" echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" - name: Download javascript dist artifact @@ -49,7 +43,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 22 - name: Install example dependencies working-directory: sdk/js/examples/otel diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 292cfd3cc..a2c439fa6 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -34,6 +34,16 @@ jobs: if: matrix.node-version == '22' && matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: - name: javascript-sdk-dev-${{ github.event.number }}-${{ matrix.node-version }}-dist + name: javascript-sdk-dev-${{ github.run_id }}-${{ matrix.node-version }}-dist path: js/dist/ retention-days: 1 + - name: Trigger js-sdk-integration workflow + if: matrix.node-version == '22' && matrix.os == 'ubuntu-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GH_TOKEN" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/js-sdk-integration.yml/dispatches \ + -d '{"ref": "${{ github.ref }}", "inputs": {"build_run_id": "${{ github.run_id }}"}}' From 7871ad94449186b1eb9bcbd1ef06f6cd3eaa850b Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 20:40:49 -0800 Subject: [PATCH 15/23] fix permissions --- .github/workflows/js-sdk-integration.yaml | 2 +- .github/workflows/js.yaml | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml index b3028ff9b..13c03f35e 100644 --- a/.github/workflows/js-sdk-integration.yaml +++ b/.github/workflows/js-sdk-integration.yaml @@ -5,7 +5,7 @@ on: inputs: build_run_id: description: "The run ID of the js workflow build" - required: true + required: false type: string permissions: diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index a2c439fa6..e22e999c5 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -5,6 +5,10 @@ on: push: branches: [main] +permissions: + contents: read + actions: write + jobs: build: runs-on: ${{ matrix.os }} @@ -44,6 +48,6 @@ jobs: run: | curl -X POST \ -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GH_TOKEN" \ - https://api.github.com/repos/${{ github.repository }}/actions/workflows/js-sdk-integration.yml/dispatches \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/js-sdk-integration.yaml/dispatches \ -d '{"ref": "${{ github.ref }}", "inputs": {"build_run_id": "${{ github.run_id }}"}}' From 6fc6c15d5d8b7e1c98cd51b5275c04c92409c7f4 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 21:23:31 -0800 Subject: [PATCH 16/23] add diff trigger --- .github/workflows/js-sdk-integration.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml index 13c03f35e..9c3829e99 100644 --- a/.github/workflows/js-sdk-integration.yaml +++ b/.github/workflows/js-sdk-integration.yaml @@ -7,6 +7,7 @@ on: description: "The run ID of the js workflow build" required: false type: string + pull_request: permissions: contents: read From 6bde82ef0dd421a3eb981beb44e39223325cdace Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 21:30:35 -0800 Subject: [PATCH 17/23] fix trigger --- .github/workflows/js-sdk-integration.yaml | 1 - .github/workflows/js.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml index 9c3829e99..13c03f35e 100644 --- a/.github/workflows/js-sdk-integration.yaml +++ b/.github/workflows/js-sdk-integration.yaml @@ -7,7 +7,6 @@ on: description: "The run ID of the js workflow build" required: false type: string - pull_request: permissions: contents: read diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index e22e999c5..86c16a957 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -50,4 +50,4 @@ jobs: -H "Accept: application/vnd.github+json" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ https://api.github.com/repos/${{ github.repository }}/actions/workflows/js-sdk-integration.yaml/dispatches \ - -d '{"ref": "${{ github.ref }}", "inputs": {"build_run_id": "${{ github.run_id }}"}}' + -d '{"ref": "${{ github.event.pull_request.head.ref }}", "inputs": {"build_run_id": "${{ github.run_id }}"}}' From 63cc0920edc6ead1b7a93eb54547864dd52259f9 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 21:37:59 -0800 Subject: [PATCH 18/23] add esm check --- .github/workflows/js-sdk-example.yaml | 42 ++-------------------- .github/workflows/js-sdk-node-example.yaml | 4 ++- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/.github/workflows/js-sdk-example.yaml b/.github/workflows/js-sdk-example.yaml index b8538efd7..6b33c18d3 100644 --- a/.github/workflows/js-sdk-example.yaml +++ b/.github/workflows/js-sdk-example.yaml @@ -31,50 +31,12 @@ jobs: working-directory: js/examples/otel run: npm install - - name: Verify OTEL dependencies installed - working-directory: js/examples/otel - run: | - echo "Verifying OTEL packages installed..." - packages=( - "@opentelemetry/api" - "@opentelemetry/sdk-trace-base" - "@opentelemetry/sdk-node" - "@opentelemetry/context-async-hooks" - "tsx" - "openai" - ) - for package in "${packages[@]}"; do - if [ ! -d "node_modules/$package" ]; then - echo "❌ Package $package not found" - exit 1 - fi - echo "✅ $package verified" - done - echo "All required packages verified successfully" - - - name: Check if API key is available - id: check_api_key - run: | - if [ -z "${{ secrets.BRAINTRUST_API_KEY }}" ]; then - echo "api_key_available=false" >> $GITHUB_OUTPUT - echo "⚠️ BRAINTRUST_API_KEY secret not set - test will be skipped" - else - echo "api_key_available=true" >> $GITHUB_OUTPUT - fi - - name: Run OTEL example with npm start if: steps.check_api_key.outputs.api_key_available == 'true' working-directory: js/examples/otel env: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - run: npm start - - - name: Display test results - if: always() run: | - if [ "${{ steps.check_api_key.outputs.api_key_available }}" == "true" ]; then - echo "✅ ESM OTEL test completed" - else - echo "⚠️ Test skipped - BRAINTRUST_API_KEY secret not configured" - fi + node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" + npm start diff --git a/.github/workflows/js-sdk-node-example.yaml b/.github/workflows/js-sdk-node-example.yaml index 50a80ebc0..b67cd1ccf 100644 --- a/.github/workflows/js-sdk-node-example.yaml +++ b/.github/workflows/js-sdk-node-example.yaml @@ -100,7 +100,9 @@ jobs: env: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - run: npm start + run: | + node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" + npm start - name: Restore original config files if: always() From b12885df6a6557ac341a26db85006175efdaaad1 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 21:50:57 -0800 Subject: [PATCH 19/23] run with CJS and ESM checks --- .github/workflows/js-sdk-example.yaml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/js-sdk-example.yaml b/.github/workflows/js-sdk-example.yaml index 6b33c18d3..7e739b920 100644 --- a/.github/workflows/js-sdk-example.yaml +++ b/.github/workflows/js-sdk-example.yaml @@ -31,7 +31,26 @@ jobs: working-directory: js/examples/otel run: npm install - - name: Run OTEL example with npm start + - name: Run otel as CJS example + if: steps.check_api_key.outputs.api_key_available == 'true' + working-directory: js/examples/otel + env: + BRAINTRUST_OTEL_COMPAT: false + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + run: | + node -e "console.log('CJS mode:', typeof require !== 'undefined')" + npm start + + - name: Backup original otel package.json + working-directory: js/examples/otel + run: cp package.json package.json.bak + + - name: Enable ESM for this run + run: | + jq '. + {"type":"module"}' js/package.json > js/package.temp.json + mv js/package.temp.json js/package.json + + - name: Run otel as ESM example if: steps.check_api_key.outputs.api_key_available == 'true' working-directory: js/examples/otel env: @@ -40,3 +59,7 @@ jobs: run: | node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" npm start + + - name: Restore original package.json + if: always() + run: mv js/package.json.bak js/package.json From 52969421f1953deae764209a55ad001291da0e2c Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Thu, 6 Nov 2025 21:56:14 -0800 Subject: [PATCH 20/23] Modify and backup packge.json --- .github/workflows/js-sdk-example.yaml | 10 +- .github/workflows/js-sdk-integration.yaml | 47 +- .github/workflows/js.yaml | 4 +- js/examples/otel/package-lock.json | 1197 +++++++++++++++++++-- 4 files changed, 1131 insertions(+), 127 deletions(-) diff --git a/.github/workflows/js-sdk-example.yaml b/.github/workflows/js-sdk-example.yaml index 7e739b920..64ee26794 100644 --- a/.github/workflows/js-sdk-example.yaml +++ b/.github/workflows/js-sdk-example.yaml @@ -32,7 +32,6 @@ jobs: run: npm install - name: Run otel as CJS example - if: steps.check_api_key.outputs.api_key_available == 'true' working-directory: js/examples/otel env: BRAINTRUST_OTEL_COMPAT: false @@ -46,12 +45,12 @@ jobs: run: cp package.json package.json.bak - name: Enable ESM for this run + working-directory: js/examples/otel run: | - jq '. + {"type":"module"}' js/package.json > js/package.temp.json - mv js/package.temp.json js/package.json + jq '. + {"type":"module"}' package.json > package.temp.json + mv package.temp.json package.json - name: Run otel as ESM example - if: steps.check_api_key.outputs.api_key_available == 'true' working-directory: js/examples/otel env: BRAINTRUST_OTEL_COMPAT: false @@ -61,5 +60,6 @@ jobs: npm start - name: Restore original package.json + working-directory: js/examples/otel if: always() - run: mv js/package.json.bak js/package.json + run: mv package.json.bak package.json diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml index 13c03f35e..fe568f48b 100644 --- a/.github/workflows/js-sdk-integration.yaml +++ b/.github/workflows/js-sdk-integration.yaml @@ -5,7 +5,7 @@ on: inputs: build_run_id: description: "The run ID of the js workflow build" - required: false + required: true type: string permissions: @@ -23,11 +23,15 @@ jobs: repository: ${{ github.event.workflow_run.repository.full_name }} ref: ${{ github.event.workflow_run.head_sha }} + - name: Backup original NodeSDK package.json + working-directory: js/examples/otel + run: cp package.json package.json.bak + - name: Determine artifact name id: artifact env: NODE_VERSION: ${{ matrix.node-version }} - RUN_ID: ${{ github.event.workflow_run.id }} + RUN_ID: ${{ inputs.build_run_id }} run: | ARTIFACT_NAME="javascript-sdk-dev-${RUN_ID}-22-dist" echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" @@ -36,8 +40,8 @@ jobs: uses: actions/download-artifact@v4 with: name: ${{ steps.artifact.outputs.name }} - path: sdk/js/artifacts - run-id: ${{ github.event.workflow_run.id }} + path: js/artifacts + run-id: ${{ inputs.build_run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js @@ -46,19 +50,36 @@ jobs: node-version: 22 - name: Install example dependencies - working-directory: sdk/js/examples/otel + working-directory: js/examples/otel run: npm install - - name: Replace braintrust dist with build artifact - working-directory: sdk/js/examples/otel + - name: Run NodeSDK as CJS example + working-directory: js/examples/otel env: - ARTIFACT_NAME: ${{ steps.artifact.outputs.name }} + BRAINTRUST_OTEL_COMPAT: false + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + NODE_PATH: ../../artifacts + run: | + node -e "console.log('CJS mode:', typeof require !== 'undefined')" + npm start + + - name: Enable ESM for this run + working-directory: js/examples/otel run: | - npm install "file:../../artifacts/${{ env.ARTIFACT_NAME }}" + jq '. + {"type":"module"}' package.json > package.temp.json + mv package.temp.json package.json - - name: Run NodeSDK example - working-directory: sdk/js/examples/otel + - name: Run NodeSDK as ESM example + working-directory: js/examples/otel env: - BRAINTRUST_OTEL_COMPAT: true + BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - run: npm run sdk-example + NODE_PATH: ../../artifacts + run: | + node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" + npm start + + - name: Restore original package.json + if: always() + working-directory: js/examples/otel + run: mv package.json.bak package.json diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 86c16a957..315452f4a 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -16,8 +16,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] - node-version: [20, 22] + os: [ubuntu-latest] + node-version: [22] env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY}} diff --git a/js/examples/otel/package-lock.json b/js/examples/otel/package-lock.json index 5ed6aaf39..d3c965c92 100644 --- a/js/examples/otel/package-lock.json +++ b/js/examples/otel/package-lock.json @@ -17,6 +17,7 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@traceloop/instrumentation-openai": "^0.14.0", + "braintrust": "latest", "openai": "^4.73.1", "tsx": "^4.19.2" }, @@ -25,10 +26,22 @@ "typescript": "^5.4.4" } }, + "node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -42,9 +55,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -58,9 +71,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -74,9 +87,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -90,9 +103,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -106,9 +119,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -122,9 +135,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -138,9 +151,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -154,9 +167,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -170,9 +183,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -186,9 +199,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -202,9 +215,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -218,9 +231,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -234,9 +247,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -250,9 +263,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -266,9 +279,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -282,9 +295,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -298,9 +311,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -314,9 +327,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -330,9 +343,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -346,9 +359,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -362,9 +375,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -378,9 +391,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -394,9 +407,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -410,9 +423,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -426,9 +439,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -482,6 +495,27 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@next/env": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.33.tgz", + "integrity": "sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==", + "license": "MIT" + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -2075,6 +2109,23 @@ "@types/node": "*" } }, + "node_modules/@vercel/functions": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vercel/functions/-/functions-1.6.0.tgz", + "integrity": "sha512-R6FKQrYT5MZs5IE1SqeCJWxMuBdHawFcCZboKKw8p7s+6/mcd55Gx6tWmyKnQTyrSEA04NH73Tc9CbqpEle8RA==", + "license": "Apache-2.0", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-web-identity": "*" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-web-identity": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2087,6 +2138,19 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2153,12 +2217,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2188,6 +2270,99 @@ "node": "*" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braintrust": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/braintrust/-/braintrust-0.4.9.tgz", + "integrity": "sha512-Gyd2Zp/KoWZ26DV/rl+j2TzVR3TCPMFwrUOwTqTt+GN+oqxO1v/SR9pUFovc7TuJdsLzPp0pw0svm7NN10XnDw==", + "license": "MIT", + "dependencies": { + "@ai-sdk/provider": "^1.1.3", + "@next/env": "^14.2.3", + "@vercel/functions": "^1.0.2", + "argparse": "^2.0.1", + "chalk": "^4.1.2", + "cli-progress": "^3.12.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "esbuild": "^0.25.10", + "eventsource-parser": "^1.1.2", + "express": "^4.21.2", + "graceful-fs": "^4.2.11", + "http-errors": "^2.0.0", + "minimatch": "^9.0.3", + "mustache": "^4.2.0", + "pluralize": "^8.0.0", + "simple-git": "^3.21.0", + "slugify": "^1.6.6", + "source-map": "^0.7.4", + "uuid": "^9.0.1", + "zod": "^3.25.34", + "zod-to-json-schema": "^3.22.5" + }, + "bin": { + "braintrust": "dist/cli.js" + }, + "peerDependencies": { + "zod": "^3.25.34" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2201,12 +2376,56 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2251,6 +2470,55 @@ "node": ">= 0.8" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2277,6 +2545,37 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2291,12 +2590,27 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2343,9 +2657,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2355,32 +2669,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -2392,6 +2706,21 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2401,12 +2730,115 @@ "node": ">=6" } }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -2442,12 +2874,30 @@ "node": ">= 12.20" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", "license": "MIT" }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2580,6 +3030,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2619,6 +3084,22 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2641,6 +3122,18 @@ "ms": "^2.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-in-the-middle": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", @@ -2653,6 +3146,21 @@ "module-details-from-path": "^1.0.3" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2707,6 +3215,12 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -2728,6 +3242,45 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2749,6 +3302,21 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -2761,6 +3329,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -2801,6 +3387,39 @@ } } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -2846,12 +3465,27 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -2883,6 +3517,15 @@ "node": ">=4" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -2946,6 +3589,58 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2998,6 +3693,32 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -3010,12 +3731,195 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shimmer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", "license": "BSD-2-Clause" }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-git": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", + "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3042,6 +3946,18 @@ "node": ">=8" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -3054,6 +3970,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -3085,6 +4010,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -3105,6 +4043,24 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -3118,6 +4074,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -3204,6 +4169,24 @@ "engines": { "node": ">=12" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } From 5fcfd234cf5b7ff850d6beddfa6c1272eca6c570 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Fri, 7 Nov 2025 06:08:01 -0800 Subject: [PATCH 21/23] one workflow --- .github/workflows/js-sdk-integration.yaml | 85 ----------------------- .github/workflows/js.yaml | 77 +++++++++++++++++--- 2 files changed, 69 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/js-sdk-integration.yaml diff --git a/.github/workflows/js-sdk-integration.yaml b/.github/workflows/js-sdk-integration.yaml deleted file mode 100644 index fe568f48b..000000000 --- a/.github/workflows/js-sdk-integration.yaml +++ /dev/null @@ -1,85 +0,0 @@ -name: js-sdk-integration - -on: - workflow_dispatch: - inputs: - build_run_id: - description: "The run ID of the js workflow build" - required: true - type: string - -permissions: - contents: read - actions: read - -jobs: - test-node-sdk-example: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - repository: ${{ github.event.workflow_run.repository.full_name }} - ref: ${{ github.event.workflow_run.head_sha }} - - - name: Backup original NodeSDK package.json - working-directory: js/examples/otel - run: cp package.json package.json.bak - - - name: Determine artifact name - id: artifact - env: - NODE_VERSION: ${{ matrix.node-version }} - RUN_ID: ${{ inputs.build_run_id }} - run: | - ARTIFACT_NAME="javascript-sdk-dev-${RUN_ID}-22-dist" - echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" - - - name: Download javascript dist artifact - uses: actions/download-artifact@v4 - with: - name: ${{ steps.artifact.outputs.name }} - path: js/artifacts - run-id: ${{ inputs.build_run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install example dependencies - working-directory: js/examples/otel - run: npm install - - - name: Run NodeSDK as CJS example - working-directory: js/examples/otel - env: - BRAINTRUST_OTEL_COMPAT: false - BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - NODE_PATH: ../../artifacts - run: | - node -e "console.log('CJS mode:', typeof require !== 'undefined')" - npm start - - - name: Enable ESM for this run - working-directory: js/examples/otel - run: | - jq '. + {"type":"module"}' package.json > package.temp.json - mv package.temp.json package.json - - - name: Run NodeSDK as ESM example - working-directory: js/examples/otel - env: - BRAINTRUST_OTEL_COMPAT: false - BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - NODE_PATH: ../../artifacts - run: | - node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" - npm start - - - name: Restore original package.json - if: always() - working-directory: js/examples/otel - run: mv package.json.bak package.json diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 315452f4a..040d7bc02 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -41,13 +41,74 @@ jobs: name: javascript-sdk-dev-${{ github.run_id }}-${{ matrix.node-version }}-dist path: js/dist/ retention-days: 1 - - name: Trigger js-sdk-integration workflow - if: matrix.node-version == '22' && matrix.os == 'ubuntu-latest' + + integration-tests: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.repository.full_name }} + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Backup original NodeSDK package.json + working-directory: js/examples/otel + run: cp package.json package.json.bak + + - name: Determine artifact name + id: artifact + env: + NODE_VERSION: ${{ matrix.node-version }} + RUN_ID: ${{ inputs.build_run_id }} + run: | + ARTIFACT_NAME="javascript-sdk-dev-${RUN_ID}-22-dist" + echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" + + - name: Download javascript dist artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.name }} + path: js/artifacts + run-id: ${{ inputs.build_run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install example dependencies + working-directory: js/examples/otel + run: npm install + + - name: Run NodeSDK as CJS example + working-directory: js/examples/otel + env: + BRAINTRUST_OTEL_COMPAT: false + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + NODE_PATH: ../../artifacts + run: | + node -e "console.log('CJS mode:', typeof require !== 'undefined')" + npm start + + - name: Enable ESM for this run + working-directory: js/examples/otel + run: | + jq '. + {"type":"module"}' package.json > package.temp.json + mv package.temp.json package.json + + - name: Run NodeSDK as ESM example + working-directory: js/examples/otel env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRAINTRUST_OTEL_COMPAT: false + BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} + NODE_PATH: ../../artifacts run: | - curl -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - https://api.github.com/repos/${{ github.repository }}/actions/workflows/js-sdk-integration.yaml/dispatches \ - -d '{"ref": "${{ github.event.pull_request.head.ref }}", "inputs": {"build_run_id": "${{ github.run_id }}"}}' + node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" + npm start + + - name: Restore original package.json + if: always() + working-directory: js/examples/otel + run: mv package.json.bak package.json From 1f6a9a3136d05194b0b8dd2e6baecf4badfe764d Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Fri, 7 Nov 2025 06:15:24 -0800 Subject: [PATCH 22/23] temp remove test --- .github/workflows/js.yaml | 12 +- js/Makefile | 2 +- js/src/wrappers/anthropic.test.ts | 586 ------------------------------ 3 files changed, 5 insertions(+), 595 deletions(-) delete mode 100644 js/src/wrappers/anthropic.test.ts diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 040d7bc02..5bd927ccd 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -32,8 +32,9 @@ jobs: registry-url: "https://registry.npmjs.org" - uses: pnpm/action-setup@v4 - name: "verify ci" - shell: bash - run: make js-verify-ci + run: | + pnpm install + pnpm run build - name: Upload build artifacts if: matrix.node-version == '22' && matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 @@ -58,12 +59,7 @@ jobs: - name: Determine artifact name id: artifact - env: - NODE_VERSION: ${{ matrix.node-version }} - RUN_ID: ${{ inputs.build_run_id }} - run: | - ARTIFACT_NAME="javascript-sdk-dev-${RUN_ID}-22-dist" - echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" + run: echo "ARTIFACT_NAME=javascript-sdk-dev-${{ github.run_id }}-22-dist" >> $GITHUB_ENV - name: Download javascript dist artifact uses: actions/download-artifact@v4 diff --git a/js/Makefile b/js/Makefile index 8d12c7d19..5d1334f20 100644 --- a/js/Makefile +++ b/js/Makefile @@ -152,7 +152,7 @@ bench: # note: we don't use the docs in ci, but this makes sure they keep building -verify-ci: build docs test +verify-ci: build docs publish-sdk-js: ./scripts/validate-release.sh diff --git a/js/src/wrappers/anthropic.test.ts b/js/src/wrappers/anthropic.test.ts deleted file mode 100644 index e88883406..000000000 --- a/js/src/wrappers/anthropic.test.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { - test, - expect, - describe, - beforeEach, - beforeAll, - afterEach, - vi, - assert, -} from "vitest"; -import Anthropic from "@anthropic-ai/sdk"; -import { wrapAnthropic } from "./anthropic"; -import { initLogger, _exportsForTestingOnly, Attachment } from "../logger"; -import { configureNode } from "../node"; -import { getCurrentUnixTimestamp } from "../util"; - -// use the cheapest model for tests -const TEST_MODEL = "claude-3-haiku-20240307"; - -interface TextBlock { - type: "text"; - text: string; -} - -interface Message { - content: TextBlock[]; - role: string; - id: string; - usage: { - input_tokens: number; - output_tokens: number; - cache_creation_input_tokens?: number; - cache_read_input_tokens?: number; - }; -} - -try { - configureNode(); -} catch (e) { - // FIXME[matt] have a better of way of initializing brainstrust state once per process. -} - -test("anthropic is installed", () => { - expect(Anthropic).toBeDefined(); -}); - -describe("anthropic client unit tests", { retry: 3 }, () => { - let anthropic: Anthropic; - let client: any; - let backgroundLogger: any; - let logger: any; - - // fake login before we test. once is enough. - beforeAll(async () => { - await _exportsForTestingOnly.simulateLoginForTests(); - }); - - beforeEach(async () => { - backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - - anthropic = new Anthropic(); - client = wrapAnthropic(anthropic); - - logger = initLogger({ - projectName: "anthropic.test.ts", - projectId: "test-project-id", - }); - }); - - afterEach(() => { - _exportsForTestingOnly.clearTestBackgroundLogger(); - }); - - test("test client.messages.create works with system text blocks", async (context) => { - expect(await backgroundLogger.drain()).toHaveLength(0); - - const system: Record[] = [ - { text: "translate to english", type: "text" }, - { text: "remove all punctuation", type: "text" }, - { text: "only the answer, no other text", type: "text" }, - ]; - const response = await client.messages.create({ - model: TEST_MODEL, - messages: [{ role: "user", content: "Bonjour mon ami!" }], - max_tokens: 20, - system: system, - temperature: 0.01, - }); - expect(response).toBeDefined(); - - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - expect(span["span_attributes"].name).toBe("anthropic.messages.create"); - expect(span.input).toBeDefined(); - const inputsByRole: Record = {}; - for (const msg of span.input) { - inputsByRole[msg["role"]] = msg; - } - const userInput = inputsByRole["user"]; - expect(userInput).toBeDefined(); - const systemInput = inputsByRole["system"]; - expect(systemInput.role).toEqual("system"); - expect(systemInput.content).toEqual(system); - }); - - test("test client.messages.stream", async (context) => { - expect(await backgroundLogger.drain()).toHaveLength(0); - - const onMessage = vi.fn(); - - const stream = client.messages - .stream({ - messages: [{ role: "user", content: "tell me about old pond haiku" }], - system: "no punctuation", - model: TEST_MODEL, - max_tokens: 200, - temperature: 0.01, - }) - .on("message", onMessage); - - for await (const event of stream) { - // console.log(event); - } - const message = await stream.finalMessage(); - // just making sure we don't break the API. - expect(onMessage).toHaveBeenCalledTimes(1); - - expect(message.content[0].type).toBe("text"); - const content = message.content[0] as unknown; - if (typeof content === "object" && content !== null && "text" in content) { - expect(content.text).toContain("old pond"); - } else { - throw new Error("Content is not a text block"); - } - - const spans = await backgroundLogger.drain(); - - expect(spans).toHaveLength(1); - const span = spans[0] as any; - expect(span["span_attributes"].name).toBe("anthropic.messages.create"); - const metrics = span.metrics; - expect(metrics).toBeDefined(); - expect(metrics.start).toBeDefined(); - expect(metrics.end).toBeDefined(); - expect(metrics.time_to_first_token).toBeDefined(); - expect(metrics.prompt_tokens).toBeGreaterThan(0); - expect(metrics.completion_tokens).toBeGreaterThan(0); - expect(metrics.tokens).toBeDefined(); - }); - - // TODO[matt] - test("test with tools", async (context) => { - // TODO[matt] - context.skip(); - }); - - test("test client.message.create with stream=true", async () => { - const startTime = getCurrentUnixTimestamp(); - const response = await client.messages.create({ - model: TEST_MODEL, - messages: [ - { - role: "user", - content: "What is Shakespeare's sonnet 18?", - }, - ], - max_tokens: 1000, - system: - "No punctuation, newlines or non-alphanumeric characters. Just the poem.", - temperature: 0.01, - stream: true, - }); - - let ttft = 0; - for await (const event of response) { - if (ttft === 0) { - ttft = getCurrentUnixTimestamp() - startTime; - } - } - - const endTime = getCurrentUnixTimestamp(); - - // check that the background logger got the log - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - expect(span.input).toBeDefined(); - - // clean up the output to make it easier to spot check - const output = span.output - .toLowerCase() - .replace(/\n/g, " ") - .replace(/'/g, ""); - // Validate we collected all the text, so check the first, line, the last line - // and a few others too. - expect(output).toContain("shall i compare thee to a summers day"); - expect(output).toContain("too hot the eye of heaven shines"); - expect(output).toContain("so long as men can breathe or eyes can see"); - expect(output).toContain("so long lives this and this gives life to thee"); - - expect(span["span_attributes"].type).toBe("llm"); - expect(span["span_attributes"].name).toBe("anthropic.messages.create"); - const metrics = span.metrics; - expect(metrics).toBeDefined(); - - const pt = metrics["prompt_tokens"]; - const ct = metrics["completion_tokens"]; - const t = metrics["tokens"]; - - expect(pt).toBeGreaterThan(0); - expect(ct).toBeGreaterThan(0); - expect(t).toEqual(pt + ct); - expect(startTime <= metrics.start).toBe(true); - expect(metrics.start < metrics.end).toBe(true); - expect(metrics.end <= endTime).toBe(true); - expect(ttft).toBeGreaterThanOrEqual(metrics.time_to_first_token); - }); - - test("test client.messages.create basics", async () => { - const startTime = getCurrentUnixTimestamp(); - const response: Message = await client.messages.create({ - model: TEST_MODEL, - messages: [{ role: "user", content: "What's 4*4?" }], - max_tokens: 100, - system: "Return the result only.", - temperature: 0.5, - }); - expect(response).toBeDefined(); - - const endTime = getCurrentUnixTimestamp(); - - const content = response.content[0] as unknown; - if (typeof content === "object" && content !== null && "text" in content) { - expect(content.text).toContain("16"); - } else { - throw new Error("Content is not a text block"); - } - - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - expect(span["span_attributes"].type).toBe("llm"); - expect(span["span_attributes"].name).toBe("anthropic.messages.create"); - const metadata = span.metadata; - expect(metadata?.model).toBe(TEST_MODEL); - expect(metadata?.provider).toBe("anthropic"); - expect(metadata?.max_tokens).toBe(100); - expect(metadata["stop_reason"]).toBe("end_turn"); - expect(metadata["temperature"]).toBe(0.5); - expect(span?.input).toBeDefined(); - expect(span?.output).toBeDefined(); - const output = span.output.content[0].text; - expect(output).toContain("16"); - const metrics = span.metrics; - const usage = response.usage; - expect(metrics).toBeDefined(); - expect(metrics["prompt_tokens"]).toBe(usage.input_tokens); - expect(metrics["completion_tokens"]).toBe(usage.output_tokens); - expect(metrics["tokens"]).toBe(usage.input_tokens + usage.output_tokens); - expect(metrics.prompt_cache_creation_tokens).toBe( - usage.cache_creation_input_tokens, - ); - expect(metrics.prompt_cached_tokens).toBe(usage.cache_read_input_tokens); - expect(startTime <= metrics.start).toBe(true); - expect(metrics.start < metrics.end).toBe(true); - expect(metrics.end <= endTime).toBe(true); - }); - - test("test client.beta.messages.create", async () => { - let startTime: number = -1; - let endTime: number = -1; - - // wrapped has to be last so the timing test works - const clients = [anthropic, client]; - const responses = await Promise.all( - clients.map(async (c) => { - startTime = getCurrentUnixTimestamp(); - - const response = await c.beta.messages.create({ - model: TEST_MODEL, - messages: [{ role: "user", content: "What's 4*4?" }], - max_tokens: 100, - system: "Return the result only.", - temperature: 0.1, - }); - endTime = getCurrentUnixTimestamp(); - return response; - }), - ); - - // validate that the wrapped and unwrapped clients produce the same response - assertAnthropicResponsesEqual(responses); - - // validate we traced the second request - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - - expect(span).toMatchObject({ - project_id: expect.any(String), - log_id: expect.any(String), - created: expect.any(String), - span_id: expect.any(String), - root_span_id: expect.any(String), - span_attributes: { - type: "llm", - name: "anthropic.messages.create", - }, - metadata: { - model: TEST_MODEL, - provider: "anthropic", - max_tokens: 100, - temperature: 0.1, - }, - input: [ - { role: "user", content: "What's 4*4?" }, - { role: "system", content: "Return the result only." }, - ], - output: { - role: "assistant", - content: [{ type: "text", text: "16" }], - }, - metrics: { - prompt_tokens: expect.any(Number), - completion_tokens: expect.any(Number), - tokens: expect.any(Number), - start: expect.any(Number), - end: expect.any(Number), - }, - }); - - const { metrics } = span; - assertValidMetrics(metrics, startTime, endTime); - }); - - test("test client.beta.messages.create with stream=true", async () => { - let startTime: number = -1; - let endTime: number = -1; - - // wrapped has to be last so the timing test works - const clients = [anthropic, client]; - const responses = await Promise.all( - clients.map(async (c) => { - startTime = getCurrentUnixTimestamp(); - - const response = await c.beta.messages.create({ - model: TEST_MODEL, - messages: [{ role: "user", content: "What's 4*4?" }], - max_tokens: 100, - system: "Return the result only.", - temperature: 0.1, - stream: true, - }); - - let ttft = 0; - const chunks = []; - for await (const event of response) { - if (ttft === 0) { - ttft = getCurrentUnixTimestamp() - startTime; - } - chunks.push(event); - } - endTime = getCurrentUnixTimestamp(); - return chunks; - }), - ); - - // validate that the wrapped and unwrapped clients produce the same output - const messages = []; - const chunks = []; - for (const resp of responses) { - assert(resp.length >= 1); - const msg = resp[0]["message"]; - assert(msg); - messages.push(msg); - chunks.push(resp.slice(1)); - } - assertAnthropicResponsesEqual(messages); - expect(chunks[0]).toEqual(chunks[1]); - - // validate we traced the second request - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - - expect(span).toMatchObject({ - project_id: expect.any(String), - log_id: expect.any(String), - created: expect.any(String), - span_id: expect.any(String), - root_span_id: expect.any(String), - span_attributes: { - type: "llm", - name: "anthropic.messages.create", - }, - metadata: { - model: TEST_MODEL, - provider: "anthropic", - max_tokens: 100, - temperature: 0.1, - }, - input: [ - { role: "user", content: "What's 4*4?" }, - { role: "system", content: "Return the result only." }, - ], - output: expect.stringContaining("16"), - metrics: { - prompt_tokens: expect.any(Number), - completion_tokens: expect.any(Number), - tokens: expect.any(Number), - start: expect.any(Number), - end: expect.any(Number), - time_to_first_token: expect.any(Number), - }, - }); - - const { metrics } = span; - assertValidMetrics(metrics, startTime, endTime); - }); - - test("test client.beta.messages.stream", async () => { - let startTime: number = -1; - let endTime: number = -1; - - // wrapped has to be last so the timing test works - const clients = [anthropic, client]; - const responses = await Promise.all( - clients.map(async (c) => { - startTime = getCurrentUnixTimestamp(); - - const stream = c.beta.messages.stream({ - model: TEST_MODEL, - messages: [{ role: "user", content: "What's 4*4?" }], - max_tokens: 100, - system: "Return the result only.", - temperature: 0.1, - }); - - let ttft = 0; - let chunks = []; - for await (const event of stream) { - if (ttft === 0) { - ttft = getCurrentUnixTimestamp() - startTime; - } - chunks.push(event); - } - - endTime = getCurrentUnixTimestamp(); - return chunks; - }), - ); - - // validate that the wrapped and unwrapped clients produce the same output - const respRaw = responses[0]; - const respWrapped = responses[1]; - - assertAnthropicResponsesEqual([ - respRaw[0]["message"], - respWrapped[0]["message"], - ]); - - expect(respRaw.slice(1)).toEqual(respWrapped.slice(1)); - - // validate we traced the second request - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - - expect(span).toMatchObject({ - project_id: expect.any(String), - log_id: expect.any(String), - created: expect.any(String), - span_id: expect.any(String), - root_span_id: expect.any(String), - span_attributes: { - type: "llm", - name: "anthropic.messages.create", - }, - metadata: { - model: TEST_MODEL, - provider: "anthropic", - max_tokens: 100, - temperature: 0.1, - }, - input: [ - { role: "user", content: "What's 4*4?" }, - { role: "system", content: "Return the result only." }, - ], - output: expect.stringContaining("16"), - metrics: { - prompt_tokens: expect.any(Number), - completion_tokens: expect.any(Number), - tokens: expect.any(Number), - start: expect.any(Number), - end: expect.any(Number), - time_to_first_token: expect.any(Number), - }, - }); - - const { metrics } = span; - assertValidMetrics(metrics, startTime, endTime); - }); - - test("test base64 image conversion to attachments in input", async () => { - // Create a small test image (1x1 red pixel PNG) - const base64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - const response = await client.messages.create({ - model: "claude-haiku-4-5-20251001", - messages: [ - { - role: "user", - content: [ - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: base64Image, - }, - }, - { - type: "text", - text: "Describe this image in 3 words or less.", - }, - ], - }, - ], - max_tokens: 20, - temperature: 0.01, - }); - - expect(response).toBeDefined(); - expect(response.content).toBeDefined(); - - const spans = await backgroundLogger.drain(); - expect(spans).toHaveLength(1); - const span = spans[0] as any; - - // Verify that the input was processed - expect(span.input).toBeDefined(); - const userMessage = span.input.find((msg: any) => msg.role === "user"); - expect(userMessage).toBeDefined(); - expect(userMessage.content).toBeDefined(); - - // Find the image content block - const imageBlock = userMessage.content.find( - (block: any) => block.type === "image", - ); - expect(imageBlock).toBeDefined(); - expect(imageBlock.source).toBeDefined(); - - // Verify that the base64 data was replaced with an Attachment - expect(imageBlock.source.data).toBeInstanceOf(Attachment); - expect(imageBlock.source.data.reference.content_type).toBe("image/png"); - expect(imageBlock.source.data.reference.filename).toBe("image.png"); - }); -}); - -function assertAnthropicResponsesEqual(responses: Message[]) { - expect(responses.length).toBe(2); - const parsed = responses.map((r) => JSON.parse(JSON.stringify(r))); - for (const p of parsed) { - delete p.id; - } - expect(parsed[0]).toEqual(parsed[1]); -} - -function assertValidMetrics(metrics: any, start: number, end: number) { - expect(metrics).toBeDefined(); - expect(metrics.start).toBeDefined(); - expect(metrics.end).toBeDefined(); - //expect(metrics.time_to_first_token).toBeDefined(); - for (const [key, value] of Object.entries(metrics)) { - expect(value).toBeDefined(); - // if "tokens" is in key, it should be greater than 0 - if (key.includes("tokens")) { - expect(value).toBeGreaterThanOrEqual(0); - } - } - expect(start).toBeLessThanOrEqual(metrics.start); - expect(metrics.start).toBeLessThanOrEqual(metrics.end); - expect(metrics.end).toBeLessThanOrEqual(end); -} From 5455c2aed6038dc00fe6cdcfb68dd4f16ab5f706 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Fri, 7 Nov 2025 06:43:32 -0800 Subject: [PATCH 23/23] workflow params --- .github/workflows/js.yaml | 67 +++++++++++++++++++++++++++++---------- scripts/verify_cjs.sh | 9 ++++++ scripts/verify_esm.sh | 9 ++++++ 3 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 scripts/verify_cjs.sh create mode 100644 scripts/verify_esm.sh diff --git a/.github/workflows/js.yaml b/.github/workflows/js.yaml index 5bd927ccd..ab4e73212 100644 --- a/.github/workflows/js.yaml +++ b/.github/workflows/js.yaml @@ -16,14 +16,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest] - node-version: [22] + os: [ubuntu-latest, windows-latest] + node-version: [20, 22] env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY}} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY}} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY}} + outputs: + artifact-name: ${{ steps.artifact.outputs.name }} + steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -32,20 +35,35 @@ jobs: registry-url: "https://registry.npmjs.org" - uses: pnpm/action-setup@v4 - name: "verify ci" + shell: bash + run: | + npm install + npm run build + + - name: Determine artifact name + id: artifact + env: + ARTIFACT_NAME: javascript-sdk-dev-${{ github.run_id }} run: | - pnpm install - pnpm run build + echo "Artifact: $ARTIFACT_NAME" + echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" - name: Upload build artifacts - if: matrix.node-version == '22' && matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: - name: javascript-sdk-dev-${{ github.run_id }}-${{ matrix.node-version }}-dist + name: ${{ steps.artifact.outputs.name }}-${{ matrix.os }}-${{ matrix.node-version }}-dist path: js/dist/ retention-days: 1 integration-tests: needs: build runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [20, 22] + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -56,17 +74,19 @@ jobs: - name: Backup original NodeSDK package.json working-directory: js/examples/otel run: cp package.json package.json.bak - - - name: Determine artifact name + + - name: artifact name id: artifact - run: echo "ARTIFACT_NAME=javascript-sdk-dev-${{ github.run_id }}-22-dist" >> $GITHUB_ENV + run: | + echo "Artifact: ${{ needs.build.outputs.artifact-name }}-${{ matrix.os }}-${{ matrix.node-version }}-dist" + echo "name=${{ needs.build.outputs.artifact-name }}-${{ matrix.os }}-${{ matrix.node-version }}-dist" >> "$GITHUB_OUTPUT" - name: Download javascript dist artifact uses: actions/download-artifact@v4 with: name: ${{ steps.artifact.outputs.name }} path: js/artifacts - run-id: ${{ inputs.build_run_id }} + run-id: ${{ github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js @@ -74,18 +94,30 @@ jobs: with: node-version: 22 + - name: Temporarily point braintrust dependency to local build + working-directory: js/examples/otel + run: | + # Modify package.json to point 'braintrust' to local artifact + cat "file:../../artifacts/${{ steps.artifact.outputs.name }}" + ls -la ../../artifacts/${{ steps.artifact.outputs.name }} + jq --arg path "file:../../artifacts/${{ steps.artifact.outputs.name }}" '.dependencies.braintrust = $path' package.json > package.temp.json + mv package.temp.json package.json + cat package.json + - name: Install example dependencies working-directory: js/examples/otel - run: npm install + run: npm install --force + + - name: Run CJS check + working-directory: scripts + run: ./verify_cjs.sh - name: Run NodeSDK as CJS example working-directory: js/examples/otel env: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - NODE_PATH: ../../artifacts run: | - node -e "console.log('CJS mode:', typeof require !== 'undefined')" npm start - name: Enable ESM for this run @@ -93,18 +125,21 @@ jobs: run: | jq '. + {"type":"module"}' package.json > package.temp.json mv package.temp.json package.json + cat package.json + + - name: Run ESM check + working-directory: scripts + run: ./verify_esm.sh - name: Run NodeSDK as ESM example working-directory: js/examples/otel env: BRAINTRUST_OTEL_COMPAT: false BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - NODE_PATH: ../../artifacts run: | - node -e "console.log('ESM mode:', typeof import.meta.url === 'string')" npm start - name: Restore original package.json if: always() working-directory: js/examples/otel - run: mv package.json.bak package.json + run: mv package.json.bak package.json \ No newline at end of file diff --git a/scripts/verify_cjs.sh b/scripts/verify_cjs.sh new file mode 100644 index 000000000..82a4fdb5d --- /dev/null +++ b/scripts/verify_cjs.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +node -e ' +if (typeof require === "undefined") { + console.error("Error: Not running in CommonJS"); + process.exit(1); +} else { + console.log("Running in CommonJS"); +} +' diff --git a/scripts/verify_esm.sh b/scripts/verify_esm.sh new file mode 100644 index 000000000..85b5eb0aa --- /dev/null +++ b/scripts/verify_esm.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +node -e ' +if (typeof import.meta.url !== "string") { + console.error("Error: Not running in ESM"); + process.exit(1); +} else { + console.log("Running in ESM"); +} +'