diff --git a/.env.example b/.env.example index d7471ce3aa..4d130ed6e6 100644 --- a/.env.example +++ b/.env.example @@ -22,5 +22,10 @@ LANGSMITH_API_KEY="" LANGSMITH_TRACING="false" # Trace transport: how spans are shipped ("native" | "otel"). EVAL_TRACE_TRANSPORT="native" +# Optional Braintrust OTEL parent override (defaults to project_name:stagehand-dev, +# or project_name:stagehand in CI). +# BRAINTRUST_OTEL_PARENT="project_name:stagehand-dev" +# Optional Braintrust OTEL traces endpoint override. +# BRAINTRUST_OTEL_URL="https://api.braintrust.dev/otel/v1/traces" # Primary trace sink when multiple are configured ("braintrust" | "langsmith"). EVAL_TRACE_PRIMARY="braintrust" diff --git a/.gitignore b/.gitignore index 5fc5921f77..535857faf5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ ctrf/ packages/evals/playwright-mcp-screenshot-*.png packages/evals/chrome-devtools-mcp-screenshot-*.png .trajectories/ +.rubric-cache/ # The browse CLI has a command source dir named "downloads" that must be tracked # despite the broad downloads/ rule above. diff --git a/packages/evals/framework/otel.ts b/packages/evals/framework/otel.ts new file mode 100644 index 0000000000..8fc47e9bf8 --- /dev/null +++ b/packages/evals/framework/otel.ts @@ -0,0 +1,149 @@ +import { ProxyTracerProvider, type Tracer } from "@opentelemetry/api"; +import { + BatchSpanProcessor, + type SpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; + +import { hasLangSmithApiKey, resolveTraceTransport } from "./langsmith.js"; + +const TRACER_NAME = "stagehand-evals"; +const SHUTDOWN_TIMEOUT_MS = 10_000; +const NOOP_TRACER = new ProxyTracerProvider().getTracer(TRACER_NAME); + +// Provider state lives on globalThis, NOT in module scope: the CLI bundle +// (esbuild --bundle) inlines this module once per import site, so module-level +// variables would give each importer its own copy — the runner would register +// the provider in one copy while tracedSpan reads NOOP from another, silently +// dropping every span. A Symbol.for-keyed global is shared across all copies. +type TracingState = { + provider: NodeTracerProvider | null; + providerPromise: Promise | null; +}; +const STATE_KEY = Symbol.for("stagehand.evals.otel.state"); +function state(): TracingState { + const g = globalThis as { [STATE_KEY]?: TracingState }; + return (g[STATE_KEY] ??= { provider: null, providerPromise: null }); +} + +/** Test-only: clear shared provider state between vitest module resets. */ +export function resetTracingStateForTests(): void { + const g = globalThis as { [STATE_KEY]?: TracingState }; + delete g[STATE_KEY]; +} + +export async function buildTracerProvider(options?: { + braintrustParent?: string; +}): Promise { + if (resolveTraceTransport() !== "otel") { + return null; + } + + const s = state(); + if (s.provider) { + return s.provider; + } + + // The provider is initialized once per process, so the first call's options win. + const pendingProvider = + s.providerPromise ?? + (s.providerPromise = initializeTracerProvider(options)); + + try { + return await pendingProvider; + } finally { + if (s.providerPromise === pendingProvider) { + s.providerPromise = null; + } + } +} + +async function initializeTracerProvider(options?: { + braintrustParent?: string; +}): Promise { + const spanProcessors: SpanProcessor[] = []; + + const braintrustApiKey = process.env.BRAINTRUST_API_KEY; + if (braintrustApiKey) { + const { OTLPTraceExporter } = await import( + "@opentelemetry/exporter-trace-otlp-proto" + ); + const braintrustProjectName = + process.env.CI === "true" ? "stagehand" : "stagehand-dev"; + const parent = + options?.braintrustParent ?? + process.env.BRAINTRUST_OTEL_PARENT ?? + `project_name:${braintrustProjectName}`; + spanProcessors.push( + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: + process.env.BRAINTRUST_OTEL_URL ?? + "https://api.braintrust.dev/otel/v1/traces", + headers: { + Authorization: `Bearer ${braintrustApiKey}`, + "x-bt-parent": parent, + }, + }), + ), + ); + } + + if (hasLangSmithApiKey() && process.env.LANGSMITH_TRACING === "true") { + const { LangSmithOTLPTraceExporter } = await import( + "langsmith/experimental/otel/exporter" + ); + spanProcessors.push( + new BatchSpanProcessor(new LangSmithOTLPTraceExporter()), + ); + } + + if (spanProcessors.length === 0) { + return null; + } + + const nextProvider = new NodeTracerProvider({ spanProcessors }); + nextProvider.register(); + state().provider = nextProvider; + return nextProvider; +} + +export function getTracer(): Tracer { + return state().provider?.getTracer(TRACER_NAME) ?? NOOP_TRACER; +} + +export async function shutdownTracing(): Promise { + const s = state(); + if (resolveTraceTransport() !== "otel" || !s.provider) { + return; + } + + const activeProvider = s.provider; + s.provider = null; + + let timeout: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout(() => { + reject( + new Error( + `Timed out shutting down tracing after ${SHUTDOWN_TIMEOUT_MS}ms.`, + ), + ); + }, SHUTDOWN_TIMEOUT_MS); + timeout.unref?.(); + }); + + try { + await Promise.race([ + activeProvider + .forceFlush() + .catch(() => {}) + .then(() => activeProvider.shutdown()), + timeoutPromise, + ]); + } finally { + if (timeout) { + clearTimeout(timeout); + } + } +} diff --git a/packages/evals/package.json b/packages/evals/package.json index fcc15adfcb..a2acfd7ef9 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -26,6 +26,10 @@ "@anthropic-ai/claude-agent-sdk": "^0.2.141", "@browserbasehq/stagehand": "workspace:*", "@openai/codex-sdk": "0.125.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", "ai": "^5.0.133", "dotenv": "^17.3.1", "langsmith": "^0.5.16", diff --git a/packages/evals/tests/framework/otel.test.ts b/packages/evals/tests/framework/otel.test.ts new file mode 100644 index 0000000000..20ecc1a2d3 --- /dev/null +++ b/packages/evals/tests/framework/otel.test.ts @@ -0,0 +1,166 @@ +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + const braintrustExporter = vi.fn(function () { + return { + export: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }; + }); + const langSmithExporter = vi.fn(function () { + return { + export: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }; + }); + const nodeTracerProvider = vi.fn(function (options: unknown) { + return { + forceFlush: vi.fn().mockResolvedValue(undefined), + getTracer: vi.fn(), + options, + register: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }; + }); + + return { + braintrustExporter, + langSmithExporter, + nodeTracerProvider, + }; +}); + +vi.mock("@opentelemetry/exporter-trace-otlp-proto", () => ({ + OTLPTraceExporter: mocks.braintrustExporter, +})); + +vi.mock("langsmith/experimental/otel/exporter", () => ({ + LangSmithOTLPTraceExporter: mocks.langSmithExporter, +})); + +vi.mock("@opentelemetry/sdk-trace-node", () => ({ + NodeTracerProvider: mocks.nodeTracerProvider, +})); + +const originalEnv = { ...process.env }; + +function constructedSpanProcessors(): unknown[] { + const options = mocks.nodeTracerProvider.mock.calls[0]?.[0] as + | { spanProcessors: unknown[] } + | undefined; + return options?.spanProcessors ?? []; +} + +describe("buildTracerProvider", () => { + beforeEach(async () => { + process.env = { ...originalEnv }; + delete process.env.BRAINTRUST_API_KEY; + delete process.env.BRAINTRUST_OTEL_PARENT; + delete process.env.BRAINTRUST_OTEL_URL; + delete process.env.CI; + delete process.env.EVAL_TRACE_TRANSPORT; + delete process.env.LANGSMITH_API_KEY; + delete process.env.LANGSMITH_TRACING; + vi.clearAllMocks(); + vi.resetModules(); + // Provider state is globalThis-backed (bundle-duplication proof), so a + // module reset alone no longer isolates tests — use the module's own + // test-reset helper to clear the shared slot. + const { resetTracingStateForTests } = await import( + "../../framework/otel.js" + ); + resetTracingStateForTests(); + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it("does not construct a provider in native mode", async () => { + const { buildTracerProvider } = await import("../../framework/otel.js"); + + await expect(buildTracerProvider()).resolves.toBeNull(); + expect(mocks.nodeTracerProvider).not.toHaveBeenCalled(); + }); + + it("constructs a provider with both backend processors", async () => { + process.env.EVAL_TRACE_TRANSPORT = "otel"; + process.env.BRAINTRUST_API_KEY = "braintrust-test-key"; + process.env.LANGSMITH_API_KEY = "langsmith-test-key"; + process.env.LANGSMITH_TRACING = "true"; + const { buildTracerProvider } = await import("../../framework/otel.js"); + + await expect(buildTracerProvider()).resolves.not.toBeNull(); + expect(mocks.braintrustExporter).toHaveBeenCalledWith({ + url: "https://api.braintrust.dev/otel/v1/traces", + headers: { + Authorization: "Bearer braintrust-test-key", + "x-bt-parent": "project_name:stagehand-dev", + }, + }); + expect(mocks.langSmithExporter).toHaveBeenCalledOnce(); + expect(constructedSpanProcessors()).toHaveLength(2); + }); + + it("constructs a provider with only the Braintrust processor", async () => { + process.env.EVAL_TRACE_TRANSPORT = "otel"; + process.env.BRAINTRUST_API_KEY = "braintrust-test-key"; + const { buildTracerProvider } = await import("../../framework/otel.js"); + + await expect(buildTracerProvider()).resolves.not.toBeNull(); + expect(mocks.braintrustExporter).toHaveBeenCalledWith({ + url: "https://api.braintrust.dev/otel/v1/traces", + headers: { + Authorization: "Bearer braintrust-test-key", + "x-bt-parent": "project_name:stagehand-dev", + }, + }); + expect(mocks.langSmithExporter).not.toHaveBeenCalled(); + expect(constructedSpanProcessors()).toHaveLength(1); + }); + + it("uses the provided Braintrust parent over the environment default", async () => { + process.env.EVAL_TRACE_TRANSPORT = "otel"; + process.env.BRAINTRUST_API_KEY = "braintrust-test-key"; + process.env.BRAINTRUST_OTEL_PARENT = "project_name:from-env"; + const { buildTracerProvider } = await import("../../framework/otel.js"); + + await expect( + buildTracerProvider({ braintrustParent: "project_name:custom" }), + ).resolves.not.toBeNull(); + expect(mocks.braintrustExporter).toHaveBeenCalledWith({ + url: "https://api.braintrust.dev/otel/v1/traces", + headers: { + Authorization: "Bearer braintrust-test-key", + "x-bt-parent": "project_name:custom", + }, + }); + }); + + it("constructs a provider with only the LangSmith processor", async () => { + process.env.EVAL_TRACE_TRANSPORT = "otel"; + process.env.LANGSMITH_API_KEY = "langsmith-test-key"; + process.env.LANGSMITH_TRACING = "true"; + const { buildTracerProvider } = await import("../../framework/otel.js"); + + await expect(buildTracerProvider()).resolves.not.toBeNull(); + expect(mocks.braintrustExporter).not.toHaveBeenCalled(); + expect(mocks.langSmithExporter).toHaveBeenCalledOnce(); + expect(constructedSpanProcessors()).toHaveLength(1); + }); + + it("shuts down the provider when forceFlush rejects", async () => { + process.env.EVAL_TRACE_TRANSPORT = "otel"; + process.env.BRAINTRUST_API_KEY = "braintrust-test-key"; + const { buildTracerProvider, shutdownTracing } = await import( + "../../framework/otel.js" + ); + + await buildTracerProvider(); + const activeProvider = mocks.nodeTracerProvider.mock.results[0]?.value; + activeProvider.forceFlush.mockRejectedValueOnce(new Error("export failed")); + + await expect(shutdownTracing()).resolves.toBeUndefined(); + expect(activeProvider.shutdown).toHaveBeenCalledOnce(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49eee60df7..dac066f17f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,6 +344,18 @@ importers: '@openai/codex-sdk': specifier: 0.125.0 version: 0.125.0 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^2.0.0 + version: 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.0.0 + version: 2.10.0(@opentelemetry/api@1.9.0) ai: specifier: ^5.0.133 version: 5.0.133(zod@4.2.1) @@ -352,7 +364,7 @@ importers: version: 17.3.1 langsmith: specifier: ^0.5.16 - version: 0.5.26(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.0(bufferutil@4.0.9))(zod@4.2.1))(ws@8.21.0(bufferutil@4.0.9)) + version: 0.5.26(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.21.0(bufferutil@4.0.9))(zod@4.2.1))(ws@8.21.0(bufferutil@4.0.9)) openai: specifier: ^4.104.0 version: 4.104.0(ws@8.21.0(bufferutil@4.0.9))(zod@4.2.1) @@ -1956,10 +1968,102 @@ packages: '@openapi-contrib/openapi-schema-to-json-schema@3.2.0': resolution: {integrity: sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw==} + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/context-async-hooks@2.10.0': + resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.10.0': + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-proto@0.220.0': + resolution: {integrity: sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.220.0': + resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.220.0': + resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.10.0': + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.220.0': + resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.9.0': + resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.10.0': + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.10.0': + resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.10.0': + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -9735,8 +9839,108 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.9.0': {} + '@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -13608,11 +13812,13 @@ snapshots: kind-of@6.0.3: {} - langsmith@0.5.26(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.0(bufferutil@4.0.9))(zod@4.2.1))(ws@8.21.0(bufferutil@4.0.9)): + langsmith@0.5.26(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.21.0(bufferutil@4.0.9))(zod@4.2.1))(ws@8.21.0(bufferutil@4.0.9)): dependencies: p-queue: 6.6.2 optionalDependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.0) openai: 4.104.0(ws@8.21.0(bufferutil@4.0.9))(zod@4.2.1) ws: 8.21.0(bufferutil@4.0.9)