diff --git a/knip.json b/knip.json index 1f3d29fa1..e605829f5 100644 --- a/knip.json +++ b/knip.json @@ -8,7 +8,7 @@ ], "workspaces": { "packages/appkit": { - "ignoreDependencies": ["vitest"] + "ignoreDependencies": ["vitest", "@databricks/sdk-experimental"] }, "packages/appkit-ui": { "ignoreDependencies": ["tailwindcss", "tw-animate-css"] diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 081132e87..a591ef9ea 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -71,6 +71,8 @@ "dependencies": { "@ast-grep/napi": "0.37.0", "@databricks/lakebase": "workspace:*", + "@databricks/sdk-experimental": "0.17.0", + "@mlflow/core": "0.4.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/auto-instrumentations-node": "0.77.0", @@ -83,8 +85,8 @@ "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", - "@opentelemetry/sdk-node": "0.219.0", "@opentelemetry/sdk-trace-base": "2.8.0", + "@opentelemetry/sdk-trace-node": "2.8.0", "@opentelemetry/semantic-conventions": "1.38.0", "@types/semver": "7.7.1", "apache-arrow": "21.1.0", @@ -95,7 +97,6 @@ "jiti": "2.6.1", "js-yaml": "4.3.1", "magic-string": "0.30.21", - "mlflow-tracing": "0.1.3", "obug": "2.1.1", "pg": "8.18.0", "picocolors": "1.1.1", diff --git a/packages/appkit/src/core/appkit.ts b/packages/appkit/src/core/appkit.ts index 0ccfd64e9..f5f4f2725 100644 --- a/packages/appkit/src/core/appkit.ts +++ b/packages/appkit/src/core/appkit.ts @@ -223,6 +223,12 @@ export class AppKit { const instance = new AppKit(mergedConfig); await Promise.all(instance.#setupPromises); + + // Build the global tracer provider now that every plugin's setup() has run + // and contributed any span processors. Deferred to here so a single provider + // carries all processors (OTLP + plugin-contributed); see TelemetryManager. + TelemetryManager.start(); + await instance.#context.emitLifecycle("setup:complete"); const handle = instance as unknown as PluginMap; diff --git a/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts b/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts index 45467625d..f644470bd 100644 --- a/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts +++ b/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts @@ -45,6 +45,8 @@ vi.mock("../../telemetry", async () => { ...actual, TelemetryManager: { initialize: vi.fn(), + start: vi.fn(), + registerSpanProcessor: vi.fn(), getProvider: () => ({ getTracer: () => ({ startActiveSpan: vi.fn((_name: string, fn: (span: any) => any) => diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index 8ebbf32c3..adcf2e077 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -64,6 +64,7 @@ import { currentTraceId, initAgentTracing, linkTraceToRun, + startAgentTracing, traceAgent, } from "./mlflow"; import { composePromptForAgent } from "./prompt"; @@ -201,6 +202,12 @@ export class AgentsPlugin extends Plugin implements ToolProvider { async setup() { await initAgentTracing(); + // Seed mlflow's config right after TelemetryManager.start() (before the + // server serves), so the first turn's request-root span is forwarded and + // that turn assembles into a trace — not dropped as a cold-start artifact. + this.context?.onLifecycle("setup:complete", () => { + startAgentTracing(); + }); const { agents, defaultAgentName } = await this.buildAgentRegistry(); this.agents = agents; this.defaultAgentName = defaultAgentName; diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index cd48b8190..5041c067d 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -1,12 +1,299 @@ +import type { UnityCatalogLocation } from "@mlflow/core"; +import { SpanKind } from "@opentelemetry/api"; +import type { SpanProcessor } from "@opentelemetry/sdk-trace-base"; + import { createLogger } from "../../logging/logger"; +import { TelemetryManager } from "../../telemetry"; const logger = createLogger("agents"); -type MlflowModule = typeof import("mlflow-tracing"); +type MlflowModule = typeof import("@mlflow/core"); +type MlflowClientInstance = InstanceType; + +interface MlflowInitConfig { + trackingUri: string; + experimentId: string; + host?: string; +} let mlflow: MlflowModule | undefined; let enabled = false; let initStarted = false; +let configured = false; +let initConfig: MlflowInitConfig | undefined; +// The resolved UC trace location, or undefined for classic experiment storage. +let ucLocation: UnityCatalogLocation | undefined; +let gatedProcessor: GatedMlflowSpanProcessor | undefined; + +/** + * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs: + * + * 1. Stay inert until ready. The classic processor's `onStart` calls its own + * `getConfig()`, which THROWS before `init()` runs — and that throw + * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit + * spans (HTTP, analytics) created between `TelemetryManager.start()` and the + * first agent turn. We contribute this to AppKit's single tracer provider + * during `setup()`, but only start forwarding once `ready()` is called by + * {@link ensureConfigured}. (The UC processor reads no global config and + * can't throw here, but forwarding is gated uniformly either way.) + * + * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY + * parentless span, and AppKit's single provider carries every HTTP/DB span — + * so unscoped, every request would become an MLflow trace. mlflow stamps + * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so + * presence alone can't tell an agent turn from a plain request — we key on + * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow + * exports the trace) only if some span in the trace carried an AGENT/TOOL + * type; otherwise `popTrace` to discard the trace mlflow built in memory. The + * real span type is set after `onStart` (the constructor stamps UNKNOWN + * first), and children end before their root, so the flag is set by the time + * the root decides. + * + * It also drops the exporters' own outbound spans at `onStart` (parentless + * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP + * shipping a trace). Forwarding those would loop: each upload is an HTTP call + * that auto-instrumentation turns into a new span to trace and upload. + * + * ponytail: non-agent requests still build (then discard) an in-memory trace + * tree — allocation-only, no network (export happens only when we forward the + * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn + * matter, root MLflow at a detached agent span instead (costs the HTTP envelope + * on the trace and splits the OTLP trace). + */ +export class GatedMlflowSpanProcessor implements SpanProcessor { + #inner: SpanProcessor; + #ready = false; + // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never + // sees an end without a matching start. + #forwarded = new WeakSet(); + // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the + // root's `onEnd` exports rather than discards. Cleared as each root ends. + #agentTraceIds = new Set(); + #popTrace: (otelTraceId: string) => void; + #spanTypeKey: string; + // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace + // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which + // mlflow stamps on all non-agent spans) is treated as non-agent. + #agentSpanTypes: ReadonlySet; + // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd. + #maxTracked: number; + // Cap on how long forceFlush/shutdown wait for a stuck export. + #flushTimeoutMs: number; + + constructor( + inner: SpanProcessor, + deps: { + popTrace: (otelTraceId: string) => void; + spanTypeKey: string; + agentSpanTypes: ReadonlySet; + maxTracked?: number; + flushTimeoutMs?: number; + }, + ) { + this.#inner = inner; + this.#popTrace = deps.popTrace; + this.#spanTypeKey = deps.spanTypeKey; + this.#agentSpanTypes = deps.agentSpanTypes; + this.#maxTracked = deps.maxTracked ?? 1024; + this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000; + } + + ready(): void { + this.#ready = true; + } + + onStart( + span: Parameters[0], + parentContext: Parameters[1], + ): void { + if (!this.#ready) return; + // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is + // an outgoing request made outside any agent turn — e.g. mlflow or OTLP + // shipping a trace. Forwarding those would loop: each upload is itself an + // HTTP call that auto-instrumentation turns into a new span to trace and + // upload. + if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) { + return; + } + this.#forwarded.add(span); + this.#inner.onStart(span, parentContext); + } + + onEnd(span: Parameters[0]): void { + if (!this.#forwarded.has(span)) return; + const traceId = span.spanContext().traceId; + // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps + // `mlflow.spanType` on every span (UNKNOWN by default), so match the value, + // not mere presence; the real type is set after `onStart`, so `onEnd` is the + // earliest we can read it. + if ( + this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) && + !this.#agentTraceIds.has(traceId) + ) { + // Normally an entry lives only until its root's onEnd deletes it. But a + // root that ends BEFORE its agent child (streaming client-disconnect) or + // never ends (crash) orphans the entry, and #agentTraceIds — unlike the + // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean. + // FIFO-evict at the cap so an abandoned-trace pattern can't grow it + // unboundedly over process uptime. + // ponytail: evicting a still-live trace only mis-discards it, and that + // needs >#maxTracked concurrent agent turns — far above real load. + if (this.#agentTraceIds.size >= this.#maxTracked) { + const oldest = this.#agentTraceIds.values().next().value; + if (oldest !== undefined) this.#agentTraceIds.delete(oldest); + } + this.#agentTraceIds.add(traceId); + } + if (span.parentSpanContext?.spanId) { + // Non-root: mlflow's own `onEnd` early-returns, but forward for balance. + this.#inner.onEnd(span); + return; + } + // Root span: export only agent traces; discard everything else so plain HTTP + // requests never become MLflow traces. `delete` reports whether it was agent. + if (this.#agentTraceIds.delete(traceId)) { + this.#inner.onEnd(span); + } else { + this.#popTrace(traceId); + } + } + + forceFlush(): Promise { + return this.#boundedFlush(() => this.#inner.forceFlush()); + } + + shutdown(): Promise { + return this.#boundedFlush(() => this.#inner.shutdown()); + } + + // Bound the inner flush/shutdown wait: both exporters export fire-and-forget, + // so a stuck export only wedges here (graceful shutdown), never a turn. + // Resolves — not rejects — on timeout: the caller is tearing down. + async #boundedFlush(op: () => Promise): Promise { + let timer: NodeJS.Timeout | undefined; + try { + await Promise.race([ + op().catch((err) => { + logger.warn("MLflow trace flush error: %O", err); + }), + new Promise((resolve) => { + timer = setTimeout(() => { + logger.warn( + "MLflow trace flush exceeded %dms; continuing (export may still be in flight)", + this.#flushTimeoutMs, + ); + resolve(); + }, this.#flushTimeoutMs); + timer.unref(); // don't keep the event loop alive on the timeout alone + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } +} + +/** + * Resolve the Unity Catalog trace location for the bound experiment, or + * `undefined` for classic experiment-backed storage. Any failure falls back to + * classic — a tracing misconfiguration must never break the agent. + * + * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` + + * `MLFLOW_UC_TABLE_PREFIX`, all three required. + * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since + * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with + * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the + * backend-populated spans/logs table names for custom-provisioned locations. + * 3. Otherwise classic. + * + * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so + * it's deep-imported like the exporter classes and covered by the tripwire test. + */ +async function resolveUcLocation( + experimentId: string, + client: MlflowClientInstance, +): Promise { + const catalogName = process.env.MLFLOW_UC_CATALOG?.trim(); + const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim(); + const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim(); + if (catalogName && schemaName && tablePrefix) { + return { catalogName, schemaName, tablePrefix }; + } + + if (!/^\d+$/.test(experimentId)) return undefined; + + try { + const experiment = await client.getExperiment(experimentId); + if (!experiment) return undefined; + const { ucLocationFromExperimentTags } = + await import("@mlflow/core/dist/core/destination"); + return ucLocationFromExperimentTags(experiment.tags) ?? undefined; + } catch (err) { + logger.warn( + "MLflow UC trace-location auto-detect failed; using classic experiment storage: %O", + err, + ); + return undefined; + } +} + +/** + * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()` + * build and globally register its own tracer provider (its own `NodeSDK`). This + * lets AppKit own the single global provider (OTLP + this processor), so agent + * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the + * global slot. + * + * When a UC trace location is bound, builds the Unity Catalog processor + + * exporter (V4 trace ids, spans uploaded to the experiment's UC table); + * otherwise the classic experiment-backed processor. `createAuthProvider`, + * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public + * in `@mlflow/core`, so only the exporter/processor classes are deep-imported — + * pinned to the exact version in package.json and guarded by a test that fails + * loudly if a version bump renames them. + * + * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope + * forwarding to agent traces: `popTrace` (to discard non-agent traces), the + * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values + * that mark a trace as an agent turn (mlflow stamps every span, defaulting to + * UNKNOWN, so the gate must match the value, not presence). + */ +async function buildMlflowSpanProcessor( + m: MlflowModule, + client: MlflowClientInstance, + ucLoc: UnityCatalogLocation | undefined, +): Promise<{ + processor: SpanProcessor; + popTrace: (otelTraceId: string) => void; + spanTypeKey: string; + agentSpanTypes: ReadonlySet; +}> { + let processor: SpanProcessor; + if (ucLoc) { + const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } = + await import("@mlflow/core/dist/exporters/uc_table"); + processor = new DatabricksUCTableSpanProcessor( + new DatabricksUCTableSpanExporter(client), + ucLoc, + ); + } else { + const { MlflowSpanExporter, MlflowSpanProcessor } = + await import("@mlflow/core/dist/exporters/mlflow"); + processor = new MlflowSpanProcessor(new MlflowSpanExporter(client)); + } + return { + processor, + popTrace: (otelTraceId) => + m.InMemoryTraceManager.getInstance().popTrace(otelTraceId), + spanTypeKey: m.SpanAttributeKey.SPAN_TYPE, + // mlflow JSON-stringifies attribute values, so the stored values are + // `"AGENT"`/`"TOOL"` (quoted). Match that exact form. + agentSpanTypes: new Set([ + JSON.stringify(m.SpanType.AGENT), + JSON.stringify(m.SpanType.TOOL), + ]), + }; +} /** The bound MLflow experiment id, from the optional `experiment` resource. */ function experimentId(): string | undefined { @@ -15,7 +302,7 @@ function experimentId(): string | undefined { } /** - * Databricks host with a scheme. The mlflow-tracing SDK uses `DATABRICKS_HOST` + * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST` * verbatim to build request URLs and doesn't add `https://`, so a bare host * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit * normalized host when the env var is set; when it isn't (profile-based auth), @@ -30,8 +317,20 @@ function normalizedDatabricksHost(): string | undefined { /** * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`). + * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`. + * + * Rather than let `@mlflow/core`'s `init()` stand up and globally register its + * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the + * span processor ourselves and contribute it to AppKit's single provider via + * {@link TelemetryManager.registerSpanProcessor}. For the classic + * experiment-backed processor, mlflow's global config is seeded by + * {@link startAgentTracing} on the `"setup:complete"` lifecycle event (after + * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the + * UC processor needs no seeded config, so that path never calls `init()`. * - * Auth is resolved by the `mlflow-tracing` SDK from the app's own Databricks + * The trace store is resolved here: a UC table prefix (env-configured or + * auto-detected from the experiment's Databricks tags) vs. the classic + * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg` * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP * headers are wired by hand. A failure (missing creds, bad experiment) logs and @@ -47,15 +346,43 @@ export async function initAgentTracing(): Promise { if (!id) return; try { - mlflow = await import("mlflow-tracing"); + mlflow = await import("@mlflow/core"); const host = normalizedDatabricksHost(); - mlflow.init({ + initConfig = { trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || "databricks", experimentId: id, ...(host ? { host } : {}), + }; + // One auth resolution + client, reused for UC auto-detect and the exporter. + const authProvider = mlflow.createAuthProvider({ + trackingUri: initConfig.trackingUri, + ...(host ? { host } : {}), + }); + const client = new mlflow.MlflowClient({ + trackingUri: initConfig.trackingUri, + authProvider, }); + ucLocation = await resolveUcLocation(id, client); + const { processor, popTrace, spanTypeKey, agentSpanTypes } = + await buildMlflowSpanProcessor(mlflow, client, ucLocation); + gatedProcessor = new GatedMlflowSpanProcessor(processor, { + popTrace, + spanTypeKey, + agentSpanTypes, + }); + TelemetryManager.registerSpanProcessor(gatedProcessor); enabled = true; - logger.info("MLflow agent tracing enabled (experiment %s)", id); + if (ucLocation) { + logger.info( + "MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)", + id, + ucLocation.catalogName, + ucLocation.schemaName, + ucLocation.tablePrefix, + ); + } else { + logger.info("MLflow agent tracing enabled (experiment %s)", id); + } } catch (err) { logger.warn("MLflow agent tracing disabled: %O", err); } @@ -71,6 +398,57 @@ export interface SpanRecorder { const noopRecorder: SpanRecorder = { setOutputs() {} }; +/** + * Seed the classic processor's global config once, AFTER + * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly + * by {@link startAgentTracing} on `"setup:complete"`, or lazily by {@link trace} + * as a fallback — then enable forwarding on the gated processor. Returns whether + * tracing is usable. + * + * The classic experiment-backed `MlflowSpanProcessor` reads + * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's + * global config. `init()` also stands up its own `NodeSDK` whose provider loses + * the global slot to AppKit's already-registered one (non-fatal); we call it + * only for that config side-effect. The UC processor carries its location and + * reads no global config, so the UC path skips `init()` entirely — no second + * `NodeSDK`, no competing global registration by `@mlflow/core`. + */ +function ensureConfigured(): boolean { + if (configured) return enabled; + configured = true; + // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw, + // `mlflow` and `initConfig` are still set but there is no gate. Calling + // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if + // AppKit registered none (no OTLP, no processor) it wins the global slot, + // routing every span into mlflow un-gated: the exact over-tracing + exporter + // loop this file exists to prevent. + if (!mlflow || !initConfig || !gatedProcessor) return false; + try { + if (!ucLocation) mlflow.init(initConfig); + gatedProcessor.ready(); + return true; + } catch (err) { + enabled = false; + logger.warn("MLflow agent tracing disabled (init failed): %O", err); + return false; + } +} + +/** + * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the + * agents plugin wires this to the `"setup:complete"` lifecycle event, before the + * server serves any request. Doing it here means the request's own root span is + * already forwarded when the first turn runs, so that turn assembles into a + * trace instead of being dropped (mlflow roots a trace only at the top-level + * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a + * turn whose agent span is itself the trace root; an HTTP-wrapped first turn + * seeded lazily loses its root span (already started — and dropped — before + * `ready()`), so this eager path is the reliable one. + */ +export function startAgentTracing(): void { + ensureConfigured(); +} + /** * Run `fn` inside an MLflow span of `spanType` when tracing is enabled, * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active @@ -86,9 +464,10 @@ async function trace( fn: (span: SpanRecorder) => Promise, ): Promise { if (!enabled || !mlflow) return fn(noopRecorder); - const type = - spanType === "AGENT" ? mlflow.SpanType.AGENT : mlflow.SpanType.TOOL; - return await mlflow.withSpan( + const m = mlflow; + if (!ensureConfigured()) return fn(noopRecorder); + const type = spanType === "AGENT" ? m.SpanType.AGENT : m.SpanType.TOOL; + return await m.withSpan( async (span) => { if (inputs !== undefined) span.setInputs(inputs); let outputsSet = false; diff --git a/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts b/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts index e57225dd2..ee438c1d6 100644 --- a/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts +++ b/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts @@ -32,6 +32,7 @@ interface FakeContext { getToolProviders(): Array<{ name: string; provider: ToolProvider }>; getPluginNames(): string[]; addRoute(): void; + onLifecycle(): void; executeTool: ( req: unknown, pluginName: string, @@ -48,6 +49,7 @@ function fakeContext( getToolProviders: () => providers, getPluginNames: () => providers.map((p) => p.name), addRoute: vi.fn(), + onLifecycle: vi.fn(), executeTool: vi.fn(async (_req, p, n, args) => ({ plugin: p, tool: n, diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index a43c29cec..6747c2b34 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -1,17 +1,39 @@ +import { SpanKind } from "@opentelemetry/api"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { GatedMlflowSpanProcessor } from "../mlflow"; + /** * The tracing module keeps module-level singleton state (`enabled`, - * `initStarted`) and lazily `import()`s `mlflow-tracing`. Each test resets the - * module registry and re-mocks the SDK so init runs fresh. + * `initStarted`) and lazily `import()`s `@mlflow/core` (plus two deep-import + * paths for the exporter/processor classes). Each test resets the module + * registry and re-mocks the SDK so init runs fresh. */ -function stubSdk(overrides: Record = {}) { +function stubSdk( + overrides: Record = {}, + ucFromTags: Record | null = null, +) { const setInputs = vi.fn(); const setOutputs = vi.fn(); const span = { setInputs, setOutputs }; const sdk = { init: vi.fn(), + // createAuthProvider, MlflowClient, InMemoryTraceManager and + // SpanAttributeKey are public on @mlflow/core's entrypoint (they were deep + // imports under mlflow-tracing 0.1.3), so they live on the main mock. + createAuthProvider: vi.fn(() => ({})), + MlflowClient: class { + // Auto-detect probes this; default: experiment exists, UC-ness is then + // decided by the ucLocationFromExperimentTags mock below. + getExperiment = vi.fn(async () => ({ + experimentId: "e", + name: "n", + tags: {}, + })); + }, + InMemoryTraceManager: { getInstance: () => ({ popTrace: vi.fn() }) }, + SpanAttributeKey: { SPAN_TYPE: "mlflow.spanType" }, SpanType: { AGENT: "AGENT", TOOL: "TOOL" }, withSpan: vi.fn(async (fn: (s: unknown) => unknown) => fn(span)), getCurrentActiveSpan: vi.fn(() => ({ traceId: "tr-active" })), @@ -21,19 +43,72 @@ function stubSdk(overrides: Record = {}) { updateCurrentTrace: vi.fn(), ...overrides, }; - vi.doMock("mlflow-tracing", () => sdk); - return { sdk, span, setInputs, setOutputs }; + vi.doMock("@mlflow/core", () => sdk); + // Deep imports used by buildMlflowSpanProcessor — the exporter/processor + // classes are the only symbols not on @mlflow/core's public entrypoint. Kept + // as light stubs so setup() wires a processor without touching Databricks. + class StubProcessor { + onStart() {} + onEnd() {} + forceFlush() { + return Promise.resolve(); + } + shutdown() { + return Promise.resolve(); + } + } + vi.doMock("@mlflow/core/dist/exporters/mlflow", () => ({ + MlflowSpanExporter: class {}, + MlflowSpanProcessor: StubProcessor, + })); + // Distinct from the classic stub so a test can tell which processor was built + // and assert the resolved UC location actually reaches its constructor. + const uc: { built: boolean; location?: unknown } = { built: false }; + vi.doMock("@mlflow/core/dist/exporters/uc_table", () => ({ + DatabricksUCTableSpanExporter: class {}, + DatabricksUCTableSpanProcessor: class { + constructor(_exporter: unknown, location: unknown) { + uc.built = true; + uc.location = location; + } + onStart() {} + onEnd() {} + forceFlush() { + return Promise.resolve(); + } + shutdown() { + return Promise.resolve(); + } + }, + })); + // mlflow's experiment-tag → UC-location parser (deep import), used by the + // numeric-id auto-detect path. Default: not a UC experiment (null → classic). + vi.doMock("@mlflow/core/dist/core/destination", () => ({ + ucLocationFromExperimentTags: vi.fn(() => ucFromTags), + })); + return { sdk, span, setInputs, setOutputs, uc }; } describe("agent tracing (mlflow)", () => { beforeEach(() => { vi.resetModules(); delete process.env.MLFLOW_EXPERIMENT_ID; + delete process.env.MLFLOW_UC_CATALOG; + delete process.env.MLFLOW_UC_SCHEMA; + delete process.env.MLFLOW_UC_TABLE_PREFIX; }); afterEach(() => { - vi.doUnmock("mlflow-tracing"); + vi.doUnmock("@mlflow/core"); + vi.doUnmock("@mlflow/core/dist/exporters/mlflow"); + vi.doUnmock("@mlflow/core/dist/exporters/uc_table"); + vi.doUnmock("@mlflow/core/dist/core/destination"); + vi.doUnmock("../../../telemetry"); + vi.restoreAllMocks(); delete process.env.MLFLOW_EXPERIMENT_ID; + delete process.env.MLFLOW_UC_CATALOG; + delete process.env.MLFLOW_UC_SCHEMA; + delete process.env.MLFLOW_UC_TABLE_PREFIX; }); test("disabled (no experiment bound): still runs fn and returns its value", async () => { @@ -46,6 +121,210 @@ describe("agent tracing (mlflow)", () => { expect(mod.currentTraceId()).toBeUndefined(); }); + test("no experiment bound: contributes no span processor", async () => { + const { TelemetryManager } = await import("../../../telemetry"); + const spy = vi + .spyOn(TelemetryManager, "registerSpanProcessor") + .mockImplementation(() => {}); + + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + expect(spy).not.toHaveBeenCalled(); + }); + + test("experiment bound: contributes a span processor during setup", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + stubSdk(); + const { TelemetryManager } = await import("../../../telemetry"); + const spy = vi + .spyOn(TelemetryManager, "registerSpanProcessor") + .mockImplementation(() => {}); + + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + expect(spy).toHaveBeenCalledOnce(); + }); + + test("classic: init() is deferred until first trace, not called during setup", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { sdk } = stubSdk(); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + expect(sdk.init).not.toHaveBeenCalled(); // deferred + + await mod.traceAgent("agent", { messages: [] }, async () => {}); + expect(sdk.init).toHaveBeenCalledOnce(); // seeded lazily on first trace + vi.doUnmock("../../../telemetry"); + }); + + test("classic: startAgentTracing seeds config eagerly, before any trace", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { sdk } = stubSdk(); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + expect(sdk.init).not.toHaveBeenCalled(); // not during setup + + // Fired from the "setup:complete" lifecycle hook, after start(), before any + // request — so the first turn's root span is already forwarded. + mod.startAgentTracing(); + expect(sdk.init).toHaveBeenCalledOnce(); + vi.doUnmock("../../../telemetry"); + }); + + test("UC via env vars: builds the UC processor with that location, never calls init()", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + process.env.MLFLOW_UC_CATALOG = "main"; + process.env.MLFLOW_UC_SCHEMA = "mario"; + process.env.MLFLOW_UC_TABLE_PREFIX = "exp-123"; + const { sdk, uc } = stubSdk(); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + // The env location reaches the UC processor verbatim... + expect(uc.built).toBe(true); + expect(uc.location).toEqual({ + catalogName: "main", + schemaName: "mario", + tablePrefix: "exp-123", + }); + // ...and the UC processor reads no global config, so init() — which would + // stand up @mlflow/core's competing NodeSDK — is never called, not even + // eagerly on "setup:complete". + expect(sdk.init).not.toHaveBeenCalled(); + vi.doUnmock("../../../telemetry"); + }); + + test("UC via experiment tag: auto-detects the location, builds the UC processor, no init()", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "123"; // numeric → auto-detect runs + const location = { + catalogName: "main", + schemaName: "mario", + tablePrefix: "123", + }; + const { sdk, uc } = stubSdk({}, location); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + expect(uc.built).toBe(true); + expect(uc.location).toEqual(location); + expect(sdk.init).not.toHaveBeenCalled(); // UC path skips init() + vi.doUnmock("../../../telemetry"); + }); + + test("numeric experiment with no UC tag: falls back to classic (init seeds config)", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "123"; + const { sdk, uc } = stubSdk(); // ucFromTags defaults to null → classic + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + expect(uc.built).toBe(false); // classic processor, not UC + expect(sdk.init).toHaveBeenCalledOnce(); // classic needs the config seed + vi.doUnmock("../../../telemetry"); + }); + + test("getExperiment failure during auto-detect: classic fallback, tracing stays enabled", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "123"; + const { sdk, uc } = stubSdk({ + MlflowClient: class { + getExperiment = vi.fn(async () => { + throw new Error("permission denied"); + }); + }, + }); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + // A UC-detect failure must never break the agent: classic fallback, enabled. + expect(uc.built).toBe(false); + expect(sdk.init).toHaveBeenCalledOnce(); + await mod.traceAgent("agent", { messages: [] }, async () => {}); + expect(sdk.withSpan).toHaveBeenCalled(); // still traces (classic) + vi.doUnmock("../../../telemetry"); + }); + + test("non-numeric experiment id: skips getExperiment (numeric-only) and uses classic", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; // non-numeric + const getExperiment = vi.fn(async () => ({ + experimentId: "x", + name: "n", + tags: {}, + })); + // ucFromTags would return a UC location if the parser were reached... + const { sdk } = stubSdk( + { + MlflowClient: class { + getExperiment = getExperiment; + }, + }, + { catalogName: "main", schemaName: "mario", tablePrefix: "x" }, + ); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + // ...but the numeric gate skips the lookup entirely, so it stays classic. + expect(getExperiment).not.toHaveBeenCalled(); + expect(sdk.init).toHaveBeenCalledOnce(); + vi.doUnmock("../../../telemetry"); + }); + + test("processor build failure: never runs mlflow.init (no ungated global provider)", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + // buildMlflowSpanProcessor throws (bad creds, or a renamed internal): the + // gate never registers, but `mlflow` and `initConfig` are already set. If + // ensureConfigured still called init(), mlflow would stand up its own + // ungated provider and (with no AppKit provider) win the global slot. + const { sdk } = stubSdk({ + createAuthProvider: () => { + throw new Error("bad creds"); + }, + }); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); // build throws, swallowed + mod.startAgentTracing(); // "setup:complete" → ensureConfigured + + expect(sdk.init).not.toHaveBeenCalled(); + vi.doUnmock("../../../telemetry"); + }); + test("currentTraceId reads the context-active span, not getLastActiveTraceId", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; const { sdk } = stubSdk(); @@ -87,4 +366,346 @@ describe("agent tracing (mlflow)", () => { content: "hi", }); }); + + // Tripwire: fails loudly if a @mlflow/core version bump moves or renames the + // deep-imported exporter/processor classes buildMlflowSpanProcessor relies on, + // or moves the public symbols we now import from the entrypoint. Runs against + // the REAL package (no mocks); an OSS trackingUri needs no Databricks creds. + test("@mlflow/core exposes the public + deep-imported symbols we construct", async () => { + const mlflow = await import("@mlflow/core"); + const { + createAuthProvider, + MlflowClient, + InMemoryTraceManager, + SpanAttributeKey, + SpanType, + } = mlflow; + const { MlflowSpanExporter, MlflowSpanProcessor } = + await import("@mlflow/core/dist/exporters/mlflow"); + const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } = + await import("@mlflow/core/dist/exporters/uc_table"); + const { ucLocationFromExperimentTags } = + await import("@mlflow/core/dist/core/destination"); + + // Public entrypoint symbols (deep imports under mlflow-tracing 0.1.3). + expect(typeof createAuthProvider).toBe("function"); + expect(typeof MlflowClient).toBe("function"); + expect(typeof InMemoryTraceManager.getInstance().popTrace).toBe("function"); + expect(SpanAttributeKey.SPAN_TYPE).toBe("mlflow.spanType"); + expect(SpanType.AGENT).toBe("AGENT"); + expect(SpanType.TOOL).toBe("TOOL"); + + // The tracing entrypoints trace()/currentTraceId()/linkTraceToRun() call. + for (const fn of [ + "init", + "withSpan", + "getCurrentActiveSpan", + "updateCurrentTrace", + ]) { + expect(typeof (mlflow as any)[fn]).toBe("function"); + } + + const authProvider = createAuthProvider({ + trackingUri: "http://localhost:5000", + }); + const client = new MlflowClient({ + trackingUri: "http://localhost:5000", + authProvider, + }); + + // Classic experiment-backed processor. + const classic = new MlflowSpanProcessor(new MlflowSpanExporter(client)); + // UC processor takes the exporter plus a UnityCatalogLocation. + const uc = new DatabricksUCTableSpanProcessor( + new DatabricksUCTableSpanExporter(client), + { catalogName: "c", schemaName: "s", tablePrefix: "p" }, + ); + for (const processor of [classic, uc]) { + for (const method of ["onStart", "onEnd", "forceFlush", "shutdown"]) { + expect(typeof (processor as any)[method]).toBe("function"); + } + } + + // getExperiment + the tag parser back UC trace-location auto-detect. + expect(typeof client.getExperiment).toBe("function"); + expect( + ucLocationFromExperimentTags({ + "mlflow.experiment.databricksTraceDestinationPath": "cat.schema.prefix", + }), + ).toMatchObject({ + catalogName: "cat", + schemaName: "schema", + tablePrefix: "prefix", + }); + expect(ucLocationFromExperimentTags({})).toBeNull(); + }); +}); + +describe("GatedMlflowSpanProcessor", () => { + const SPAN_TYPE_KEY = "mlflow.spanType"; + // The JSON-stringified AGENT/TOOL values that mark a trace as an agent turn, + // exactly as buildMlflowSpanProcessor computes them. mlflow stamps UNKNOWN on + // every non-agent span, so this set must NOT match `'"UNKNOWN"'`. + const AGENT_SPAN_TYPES = new Set(['"AGENT"', '"TOOL"']); + + function mkGated(maxTracked?: number) { + const inner = { + onStart: vi.fn(), + onEnd: vi.fn(), + forceFlush: vi.fn(() => Promise.resolve()), + shutdown: vi.fn(() => Promise.resolve()), + }; + const popTrace = vi.fn(); + const gated = new GatedMlflowSpanProcessor(inner as any, { + popTrace, + spanTypeKey: SPAN_TYPE_KEY, + agentSpanTypes: AGENT_SPAN_TYPES, + maxTracked, + }); + return { inner, popTrace, gated }; + } + + // Defaults to an in-turn child span (INTERNAL, has a parent). Override + // kind/parentSpanContext/attributes/traceId for other cases. + const mkSpan = (over: Record = {}) => ({ + name: "s", + kind: SpanKind.INTERNAL, + parentSpanContext: { spanId: "parent" }, + attributes: {} as Record, + spanContext: () => ({ traceId: (over.traceId as string) ?? "tr-1" }), + ...over, + }); + + test("stays inert before ready(): no forwarding, so onStart can't throw on early spans", () => { + const { inner, gated } = mkGated(); + const span = mkSpan(); + + gated.onStart(span as any, {} as any); + gated.onEnd(span as any); + + expect(inner.onStart).not.toHaveBeenCalled(); + expect(inner.onEnd).not.toHaveBeenCalled(); + }); + + test("exports the trace when it contains an mlflow (agent) span", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + + const requestRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + // mlflow stamps the SERVER root UNKNOWN; the AGENT child is what counts. + attributes: { [SPAN_TYPE_KEY]: '"UNKNOWN"' }, + traceId: "tr-agent", + }); // incoming request — mlflow roots the trace here + const agentChild = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-agent", + }); + + // Real nesting order: root starts, child starts, child ends, root ends last. + gated.onStart(requestRoot as any, {} as any); + gated.onStart(agentChild as any, {} as any); + gated.onEnd(agentChild as any); + gated.onEnd(requestRoot as any); + + expect(inner.onStart).toHaveBeenCalledTimes(2); + expect(inner.onEnd).toHaveBeenCalledWith(requestRoot); // exported + expect(popTrace).not.toHaveBeenCalled(); + }); + + // Regression: mlflow's real processor stamps `mlflow.spanType = "UNKNOWN"` on + // EVERY span it processes, so a presence check (`!== undefined`) would export + // every plain request. The gate must key on the AGENT/TOOL value instead. + test("discards a plain request even though mlflow stamps every span UNKNOWN", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + + const requestRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + attributes: { [SPAN_TYPE_KEY]: '"UNKNOWN"' }, // mlflow stamps the root + traceId: "tr-plain", + }); // e.g. /api/analytics/query + const dbChild = mkSpan({ + kind: SpanKind.CLIENT, + attributes: { [SPAN_TYPE_KEY]: '"UNKNOWN"' }, // ...and the child + traceId: "tr-plain", + }); // SQL warehouse call + + gated.onStart(requestRoot as any, {} as any); + gated.onStart(dbChild as any, {} as any); + gated.onEnd(dbChild as any); + gated.onEnd(requestRoot as any); + + // UNKNOWN is not AGENT/TOOL → discarded from mlflow, not exported. + expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-plain"); + expect(inner.onEnd).not.toHaveBeenCalledWith(requestRoot); + }); + + test("scopes per-trace under interleaved concurrent traffic (not a single flag)", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + + const agentRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-A", + }); + const agentChild = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-A", + }); + const plainRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-B", + }); + const dbChild = mkSpan({ kind: SpanKind.CLIENT, traceId: "tr-B" }); + + // Both traces in flight; the plain one (tr-B) finishes first, the agent one last. + gated.onStart(agentRoot as any, {} as any); + gated.onStart(plainRoot as any, {} as any); + gated.onStart(agentChild as any, {} as any); + gated.onStart(dbChild as any, {} as any); + gated.onEnd(dbChild as any); + gated.onEnd(plainRoot as any); + gated.onEnd(agentChild as any); + gated.onEnd(agentRoot as any); + + // A single boolean 'sawAgentSpan' flag would cross-contaminate these. + expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-B"); + expect(inner.onEnd).toHaveBeenCalledWith(agentRoot); + expect(inner.onEnd).not.toHaveBeenCalledWith(plainRoot); + }); + + test("root ending before its agent child (streaming disconnect): discards that turn, stays balanced", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + const root = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-race", + }); + const agentChild = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-race", + }); + + gated.onStart(root as any, {} as any); + gated.onStart(agentChild as any, {} as any); + // HTTP root ends before the agent span's finally runs; the late child must + // not throw. Documented tradeoff: the aborted turn's trace is discarded. + gated.onEnd(root as any); + gated.onEnd(agentChild as any); + + expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-race"); + expect(inner.onEnd).not.toHaveBeenCalledWith(root); + }); + + test("bounds #agentTraceIds so an abandoned-trace pattern can't leak unboundedly", () => { + const { popTrace, gated } = mkGated(2); // cap = 2 + gated.ready(); + + // Three agent children whose roots never end — orphans their trace ids. + for (const id of ["tr-1", "tr-2", "tr-3"]) { + const child = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: id, + }); + gated.onStart(child as any, {} as any); + gated.onEnd(child as any); + } + + // tr-1 was FIFO-evicted at the cap, so if its root ever does arrive it's + // treated as non-agent (mis-discarded) rather than leaking forever. + const root1 = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-1", + }); + gated.onStart(root1 as any, {} as any); + gated.onEnd(root1 as any); + + expect(popTrace).toHaveBeenCalledWith("tr-1"); + }); + + test("agent span that is itself the root (no HTTP wrapper, e.g. eval/CLI) is exported", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + // No SERVER wrapper: the AGENT span is the parentless root and marks itself, + // so the same onEnd both adds the traceId and hits the root branch. + const agentRoot = mkSpan({ + kind: SpanKind.INTERNAL, + parentSpanContext: undefined, + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-eval", + }); + + gated.onStart(agentRoot as any, {} as any); + gated.onEnd(agentRoot as any); + + expect(inner.onEnd).toHaveBeenCalledWith(agentRoot); // exported + expect(popTrace).not.toHaveBeenCalled(); + }); + + test("drops the exporters' own outbound spans (parentless CLIENT) — breaks the loop", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + // An mlflow/OTLP upload: outgoing HTTP with no parent (made outside any turn). + const uploadSpan = mkSpan({ + kind: SpanKind.CLIENT, + parentSpanContext: undefined, + traceId: "tr-upload", + }); + + gated.onStart(uploadSpan as any, {} as any); + gated.onEnd(uploadSpan as any); + + expect(inner.onStart).not.toHaveBeenCalled(); + expect(inner.onEnd).not.toHaveBeenCalled(); + expect(popTrace).not.toHaveBeenCalled(); // never registered, nothing to discard + }); + + test("onEnd is skipped for spans whose onStart was not forwarded (balanced)", () => { + const { inner, popTrace, gated } = mkGated(); + const early = mkSpan(); + + gated.onStart(early as any, {} as any); // dropped (not ready) + gated.ready(); + gated.onEnd(early as any); // must NOT forward — inner never saw its start + + expect(inner.onEnd).not.toHaveBeenCalled(); + expect(popTrace).not.toHaveBeenCalled(); + }); + + test("delegates forceFlush and shutdown to the inner processor", async () => { + const { inner, gated } = mkGated(); + + await gated.forceFlush(); + await gated.shutdown(); + + expect(inner.forceFlush).toHaveBeenCalledOnce(); + expect(inner.shutdown).toHaveBeenCalledOnce(); + }); + + test("bounds a stuck flush: resolves without hanging when the inner flush never settles", async () => { + const inner = { + onStart: vi.fn(), + onEnd: vi.fn(), + // Never resolves — simulates a wedged UC export. + forceFlush: vi.fn(() => new Promise(() => {})), + shutdown: vi.fn(() => new Promise(() => {})), + }; + const gated = new GatedMlflowSpanProcessor(inner as any, { + popTrace: vi.fn(), + spanTypeKey: SPAN_TYPE_KEY, + agentSpanTypes: AGENT_SPAN_TYPES, + flushTimeoutMs: 5, + }); + + // Both resolve via the timeout rather than hanging on the inner promise. + await expect(gated.forceFlush()).resolves.toBeUndefined(); + await expect(gated.shutdown()).resolves.toBeUndefined(); + }); }); diff --git a/packages/appkit/src/telemetry/telemetry-manager.ts b/packages/appkit/src/telemetry/telemetry-manager.ts index b19cd1a07..3d3815610 100644 --- a/packages/appkit/src/telemetry/telemetry-manager.ts +++ b/packages/appkit/src/telemetry/telemetry-manager.ts @@ -1,3 +1,5 @@ +import { metrics } from "@opentelemetry/api"; +import { logs } from "@opentelemetry/api-logs"; import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; @@ -14,9 +16,19 @@ import { type Resource, resourceFromAttributes, } from "@opentelemetry/resources"; -import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; -import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; -import { NodeSDK } from "@opentelemetry/sdk-node"; +import { + BatchLogRecordProcessor, + LoggerProvider, +} from "@opentelemetry/sdk-logs"; +import { + MeterProvider, + PeriodicExportingMetricReader, +} from "@opentelemetry/sdk-metrics"; +import { + BatchSpanProcessor, + type SpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, @@ -30,12 +42,34 @@ import type { TelemetryConfig } from "./types"; const logger = createLogger("telemetry"); +/** + * Owns the app's OpenTelemetry providers, split into two phases so plugins can + * contribute trace span processors before the tracer provider is built. + * + * - `initialize()` runs at app bootstrap, before plugin setup. It registers the + * meter and logger providers eagerly, because OTel's metrics API has no lazy + * proxy: a counter/histogram bound against the NoOp meter (as every connector + * and the cache do in their constructors) stays NoOp for the process lifetime. + * It does NOT register a tracer provider. + * - `registerSpanProcessor()` is called by plugins during `setup()` to add a + * span processor (e.g. an MLflow exporter) to the not-yet-built tracer. + * - `start()` runs after all plugin `setup()` completes. It builds the single + * global tracer provider with the OTLP processor (if configured) plus every + * contributed processor. Deferring is safe for traces: OTel's ProxyTracer + * rebinds tracers obtained before registration, and no span is emitted during + * setup. + */ export class TelemetryManager { private static readonly DEFAULT_EXPORT_INTERVAL_MS = 10000; private static readonly DEFAULT_FALLBACK_APP_NAME = "databricks-app"; private static instance?: TelemetryManager; - private sdk?: NodeSDK; + private resource?: Resource; + private meterProvider?: MeterProvider; + private loggerProvider?: LoggerProvider; + private tracerProvider?: NodeTracerProvider; + private readonly spanProcessors: SpanProcessor[] = []; + private started = false; private shutdownPromise?: Promise; /** @@ -67,20 +101,49 @@ export class TelemetryManager { instance._initialize(config); } + /** + * Contribute a span processor to the not-yet-built tracer provider. Called by + * plugins during `setup()`. No-op with a warning once `start()` has run, since + * a started provider's processors are immutable in OTel JS 2.x. + */ + static registerSpanProcessor(processor: SpanProcessor): void { + TelemetryManager.getInstance()._registerSpanProcessor(processor); + } + + private _registerSpanProcessor(processor: SpanProcessor): void { + if (this.started) { + logger.warn( + "registerSpanProcessor called after start(); processor ignored. " + + "Contribute span processors during plugin setup().", + ); + return; + } + this.spanProcessors.push(processor); + } + + /** + * Phase 1: register the meter and logger providers eagerly (before plugin + * setup), so metric instruments bound in connector/cache constructors attach + * to real meters. The tracer provider is deferred to `start()`. + * + * When no OTLP endpoint is configured, meter/logger registration is skipped; + * a contributed span processor can still bring up tracing in `start()`. + */ private _initialize(config: Partial): void { - if (this.sdk) return; + if (this.resource) return; + this.resource = this.createResource(config); + // OTLP exporters need an endpoint. Without one there is nothing to export + // metrics/logs to, so skip those providers — but still capture the resource + // and let `start()` bring up a tracer if a plugin contributed a processor. if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { return; } try { - this.sdk = new NodeSDK({ - resource: this.createResource(config), - autoDetectResources: false, - sampler: new AppKitSampler(), - traceExporter: new OTLPTraceExporter({ headers: config.headers }), - metricReaders: [ + this.meterProvider = new MeterProvider({ + resource: this.resource, + readers: [ new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ headers: config.headers }), exportIntervalMillis: @@ -88,21 +151,72 @@ export class TelemetryManager { TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS, }), ], - logRecordProcessors: [ + }); + metrics.setGlobalMeterProvider(this.meterProvider); + + this.loggerProvider = new LoggerProvider({ + resource: this.resource, + processors: [ new BatchLogRecordProcessor( new OTLPLogExporter({ headers: config.headers }), ), ], - instrumentations: this.getDefaultInstrumentations(), }); + logs.setGlobalLoggerProvider(this.loggerProvider); + + // The OTLP trace exporter is the first span processor; contributed + // processors join it in `start()`. + this.spanProcessors.push( + new BatchSpanProcessor( + new OTLPTraceExporter({ headers: config.headers }), + ), + ); - this.sdk.start(); - logger.debug("Initialized successfully"); + this.registerInstrumentations(this.getDefaultInstrumentations()); + logger.debug("Meter/logger providers initialized"); } catch (error) { logger.error("Failed to initialize: %O", error); } } + /** + * Phase 2: build and register the global tracer provider. Called by core + * after every plugin's `setup()` completes, so all contributed span + * processors are known. No-op when nothing needs tracing (no OTLP endpoint + * and no contributed processor), preserving "no telemetry unless configured". + * + * `NodeTracerProvider.register()` installs the async-hooks context manager and + * W3C propagators — the same wiring `NodeSDK.start()` did — so span nesting + * across awaits is preserved. + */ + static start(): void { + TelemetryManager.getInstance()._start(); + } + + private _start(): void { + if (this.started) return; + this.started = true; + + if (this.spanProcessors.length === 0) { + return; + } + + try { + this.tracerProvider = new NodeTracerProvider({ + resource: this.resource, + sampler: new AppKitSampler(), + spanProcessors: this.spanProcessors, + }); + this.tracerProvider.register(); + logger.debug( + "Tracer provider started with %d span processor(s)", + this.spanProcessors.length, + ); + } catch (error) { + logger.error("Failed to start tracer provider: %O", error); + } + } + /** * Register OpenTelemetry instrumentations. * Can be called at any time, but recommended to call in plugin constructor. @@ -110,7 +224,8 @@ export class TelemetryManager { */ registerInstrumentations(instrumentations: Instrumentation[]): void { otelRegisterInstrumentations({ - // global providers set by NodeSDK.start() + // Instrumentations bind to the global providers registered by start() + // (tracer) and _initialize() (meter/logger). instrumentations, }); } @@ -160,23 +275,34 @@ export class TelemetryManager { } /** - * Flush and shut down the OpenTelemetry SDK. + * Flush and shut down the tracer, meter, and logger providers. * - * Idempotent: the SDK reference is cleared synchronously and concurrent + * Idempotent: the provider references are cleared synchronously and concurrent * or repeated calls await the same in-flight flush. Awaited by the core * lifecycle manager during graceful shutdown — that manager owns the * process signal handlers, so telemetry no longer registers its own. */ async shutdown(): Promise { - if (this.sdk) { - const sdk = this.sdk; - this.sdk = undefined; + const providers = [ + this.tracerProvider, + this.meterProvider, + this.loggerProvider, + ].filter((p): p is NonNullable => p !== undefined); + + if (providers.length > 0) { + this.tracerProvider = undefined; + this.meterProvider = undefined; + this.loggerProvider = undefined; this.shutdownPromise = (async () => { - try { - await sdk.shutdown(); - } catch (error) { - logger.error("Error shutting down: %O", error); - } + await Promise.all( + providers.map(async (provider) => { + try { + await provider.shutdown(); + } catch (error) { + logger.error("Error shutting down: %O", error); + } + }), + ); })(); } diff --git a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts index 84be228b8..75453c877 100644 --- a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts +++ b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts @@ -1,3 +1,5 @@ +import { context, metrics, trace } from "@opentelemetry/api"; +import { logs } from "@opentelemetry/api-logs"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { TelemetryManager } from "../telemetry-manager"; @@ -55,12 +57,21 @@ describe("TelemetryManager", () => { vi.clearAllMocks(); // @ts-expect-error - accessing private static property for testing TelemetryManager.instance = undefined; - // @ts-expect-error - accessing private static property for testing - TelemetryManager.shutdownRegistered = false; + // OTel's registerGlobal is allowOverride=false: a global registered by one + // test would make the next test's registration a silent no-op. Reset all + // global providers so each test starts clean. + trace.disable(); + metrics.disable(); + logs.disable(); + context.disable(); }); afterEach(() => { process.env = originalEnv; + trace.disable(); + metrics.disable(); + logs.disable(); + context.disable(); }); test("getInstance() should return singleton instance", () => { @@ -90,6 +101,7 @@ describe("TelemetryManager", () => { serviceName: "integration-test", serviceVersion: "1.0.0", }); + TelemetryManager.start(); const telemetryProvider = TelemetryManager.getProvider("test-plugin"); const tracer = telemetryProvider.getTracer(); @@ -186,6 +198,7 @@ describe("TelemetryManager", () => { serviceName: "span-test", serviceVersion: "1.0.0", }); + TelemetryManager.start(); const telemetryProvider = TelemetryManager.getProvider("span-test-plugin"); @@ -211,6 +224,7 @@ describe("TelemetryManager", () => { serviceName: "error-test", serviceVersion: "1.0.0", }); + TelemetryManager.start(); const telemetryProvider = TelemetryManager.getProvider("error-test-plugin"); @@ -224,4 +238,108 @@ describe("TelemetryManager", () => { ).rejects.toThrow("Test error in span"); }); }); + + describe("two-phase init (registerSpanProcessor + start)", () => { + /** Minimal SpanProcessor that records the names of spans it sees start. */ + function recordingProcessor() { + const startedSpans: string[] = []; + return { + startedSpans, + onStart: (span: { name: string }) => { + startedSpans.push(span.name); + }, + onEnd: () => {}, + forceFlush: () => Promise.resolve(), + shutdown: () => Promise.resolve(), + }; + } + + test("routes spans to a contributed processor with no OTLP endpoint", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const processor = recordingProcessor(); + + TelemetryManager.initialize({ serviceName: "contrib-only" }); + TelemetryManager.registerSpanProcessor(processor as any); + TelemetryManager.start(); + + const tracer = TelemetryManager.getProvider("contrib-plugin").getTracer(); + await tracer.startActiveSpan("contributed.span", {}, async (span) => { + span.end(); + }); + + expect(processor.startedSpans).toContain("contributed.span"); + }); + + test("start() is idempotent (no rebuild / double-attach)", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const processor = recordingProcessor(); + + TelemetryManager.initialize({ serviceName: "idempotent" }); + TelemetryManager.registerSpanProcessor(processor as any); + TelemetryManager.start(); + TelemetryManager.start(); // second call must not rebuild or re-attach + + const tracer = + TelemetryManager.getProvider("idempotent-plugin").getTracer(); + await tracer.startActiveSpan("once.span", {}, async (span) => { + span.end(); + }); + + // A provider rebuilt on the second start() would double-record the span. + expect(processor.startedSpans).toEqual(["once.span"]); + }); + + test("registerSpanProcessor after start() is ignored (not attached)", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const early = recordingProcessor(); + const late = recordingProcessor(); + + TelemetryManager.initialize({ serviceName: "late-register" }); + TelemetryManager.registerSpanProcessor(early as any); + TelemetryManager.start(); + TelemetryManager.registerSpanProcessor(late as any); + + const tracer = TelemetryManager.getProvider("late-plugin").getTracer(); + await tracer.startActiveSpan("post.start.span", {}, async (span) => { + span.end(); + }); + + expect(early.startedSpans).toContain("post.start.span"); + expect(late.startedSpans).toHaveLength(0); + }); + + test("start() with no OTLP endpoint and no processors is a no-op", () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + + const before = trace.getTracerProvider(); + TelemetryManager.initialize({ serviceName: "no-telemetry" }); + TelemetryManager.start(); + + // Nothing to export → the spanProcessors.length===0 early return means NO + // global tracer provider was registered (the API still returns the proxy). + expect(trace.getTracerProvider()).toBe(before); + }); + + test("metrics obtained after initialize() (before start()) still record", () => { + // Guards the eager-metrics invariant: the meter provider is registered in + // initialize(), not start(), because OTel's metrics API has no lazy proxy. + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; + + const noop = metrics.getMeterProvider(); + TelemetryManager.initialize({ serviceName: "eager-metrics" }); + // A REAL meter provider is registered by initialize(), before start() — + // not the NoOp. A `counter.add(...).not.toThrow()` assertion would pass + // against the NoOp meter too, so it can't detect a regression that moved + // registration into start(); this comparison can. + expect(metrics.getMeterProvider()).not.toBe(noop); + + // Instrument bound BEFORE start() — mirrors connector/cache constructors. + const meter = TelemetryManager.getProvider("metrics-plugin").getMeter(); + const counter = meter.createCounter("eager.counter"); + + TelemetryManager.start(); + + expect(() => counter.add(1, { label: "value" })).not.toThrow(); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1657de0f8..5ee76abd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,6 +258,12 @@ importers: '@databricks/lakebase': specifier: workspace:* version: link:../lakebase + '@databricks/sdk-experimental': + specifier: 0.17.0 + version: 0.17.0 + '@mlflow/core': + specifier: 0.4.0 + version: 0.4.0(bufferutil@4.0.9) '@opentelemetry/api': specifier: 1.9.0 version: 1.9.0 @@ -294,12 +300,12 @@ importers: '@opentelemetry/sdk-metrics': specifier: 2.8.0 version: 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': - specifier: 0.219.0 - version: 0.219.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': specifier: 2.8.0 version: 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: 2.8.0 + version: 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: 1.38.0 version: 1.38.0 @@ -330,9 +336,6 @@ importers: magic-string: specifier: 0.30.21 version: 0.30.21 - mlflow-tracing: - specifier: 0.1.3 - version: 0.1.3 obug: specifier: 2.1.1 version: 2.1.1 @@ -2603,6 +2606,18 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -2642,6 +2657,9 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@kubernetes/client-node@1.4.0': + resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -2663,6 +2681,11 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@mlflow/core@0.4.0': + resolution: {integrity: sha512-z80wUaXWRsw5xquvpBW2BRJgjA7M+dj67JqR08i+cH8DZK+DV5y8RpaJyFzxGZkAXsy5GO4W+ndeL8tB5nlQaw==} + engines: {node: '>=18'} + hasBin: true + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -5360,6 +5383,9 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} @@ -5461,6 +5487,9 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + '@types/stream-buffers@3.0.8': + resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5859,6 +5888,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} @@ -5880,6 +5912,14 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -5918,6 +5958,43 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.2: + resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6293,6 +6370,10 @@ packages: resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} engines: {node: '>=10'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -6924,6 +7005,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -7276,6 +7361,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -7427,6 +7516,9 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -7474,6 +7566,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -7613,6 +7708,10 @@ packages: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -7885,6 +7984,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -8012,6 +8115,10 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -8455,6 +8562,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + issue-parser@7.0.1: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} engines: {node: ^18.17 || >=20.6.1} @@ -8505,6 +8617,9 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + jose@6.2.10: + resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8528,6 +8643,10 @@ packages: canvas: optional: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -8581,6 +8700,11 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + just-diff-apply@5.5.0: resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} @@ -9268,10 +9392,6 @@ packages: engines: {node: '>=10'} hasBin: true - mlflow-tracing@0.1.3: - resolution: {integrity: sha512-Koqkwaid5ubGHuLprBP6J7Su70WddlD11f2vgzgxbFFHYKsAsJatMGvjIck5CkyhT/gMUyBqpA3Lkl+zC3W3uQ==} - engines: {node: '>=18'} - mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -9369,6 +9489,15 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9466,6 +9595,9 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true + oauth4webapi@3.8.7: + resolution: {integrity: sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9530,6 +9662,9 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + openid-client@6.8.7: + resolution: {integrity: sha512-gtKthNu7evSBvTdrrlHb4F3Fi9dcwlb5QaITlCs+9mfpvuOi0Q3qtBf5+iY4sEP8hy1qCoAdxBNPDcmZeVSDzQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -10665,6 +10800,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -11129,6 +11267,13 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -11275,14 +11420,23 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} + tar@7.5.10: resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} engines: {node: '>=18'} + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -11308,6 +11462,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -11397,6 +11554,9 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -12044,6 +12204,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.0: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} @@ -12124,6 +12287,9 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -15175,6 +15341,16 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + optional: true + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + optional: true + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -15213,6 +15389,34 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@kubernetes/client-node@1.4.0(bufferutil@4.0.9)': + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 24.10.1 + '@types/node-fetch': 2.6.13 + '@types/stream-buffers': 3.0.8 + form-data: 4.0.6 + hpagent: 1.2.0 + isomorphic-ws: 5.0.0(ws@8.21.0(bufferutil@4.0.9)) + js-yaml: 4.3.1 + jsonpath-plus: 10.4.0 + node-fetch: 2.7.0 + openid-client: 6.8.7 + rfc4648: 1.5.4 + socks-proxy-agent: 8.0.5 + stream-buffers: 3.0.3 + tar-fs: 3.1.3 + ws: 8.21.0(bufferutil@4.0.9) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + optional: true + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -15265,6 +15469,28 @@ snapshots: dependencies: langium: 3.3.1 + '@mlflow/core@0.4.0(bufferutil@4.0.9)': + dependencies: + '@databricks/sdk-experimental': 0.15.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/exporter-trace-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) + bignumber.js: 9.3.1 + fast-safe-stringify: 2.1.1 + ini: 5.0.0 + optionalDependencies: + '@kubernetes/client-node': 1.4.0(bufferutil@4.0.9) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.8.1 @@ -18007,6 +18233,12 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 25.2.3 + form-data: 4.0.6 + optional: true + '@types/node-forge@1.3.14': dependencies: '@types/node': 25.2.3 @@ -18131,6 +18363,11 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/stream-buffers@3.0.8': + dependencies: + '@types/node': 25.2.3 + optional: true + '@types/tedious@4.0.14': dependencies: '@types/node': 25.2.3 @@ -18636,6 +18873,9 @@ snapshots: dependencies: retry: 0.13.1 + asynckit@0.4.0: + optional: true + autocomplete.js@0.37.1: dependencies: immediate: 3.3.0 @@ -18663,6 +18903,9 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + b4a@1.8.1: + optional: true + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 @@ -18706,6 +18949,40 @@ snapshots: balanced-match@4.0.4: {} + bare-events@2.9.2: + optional: true + + bare-fs@4.8.1: + dependencies: + bare-events: 2.9.2 + bare-path: 3.1.1 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-path@3.1.1: + optional: true + + bare-stream@2.13.4(bare-events@2.9.2): + dependencies: + b4a: 1.8.1 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - react-native-b4a + optional: true + + bare-url@2.5.2: + dependencies: + bare-path: 3.1.1 + optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.8.32: {} @@ -19165,6 +19442,11 @@ snapshots: combine-promises@1.2.0: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + optional: true + comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -19824,6 +20106,9 @@ snapshots: dependencies: robust-predicates: 3.0.2 + delayed-stream@1.0.0: + optional: true + depd@1.1.2: {} depd@2.0.0: {} @@ -20066,6 +20351,14 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + optional: true + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -20273,6 +20566,13 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - bare-abort-controller + optional: true + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -20357,6 +20657,9 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: + optional: true + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -20496,6 +20799,15 @@ snapshots: form-data-encoder@4.1.0: {} + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + optional: true + format@0.2.2: {} formatly@0.3.0: @@ -20816,6 +21128,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + optional: true + hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -21113,6 +21430,9 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 + hpagent@1.2.0: + optional: true + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -21490,6 +21810,11 @@ snapshots: isobject@3.0.1: {} + isomorphic-ws@5.0.0(ws@8.21.0(bufferutil@4.0.9)): + dependencies: + ws: 8.21.0(bufferutil@4.0.9) + optional: true + issue-parser@7.0.1: dependencies: lodash.capitalize: 4.2.1 @@ -21569,6 +21894,9 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + jose@6.2.10: + optional: true + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -21610,6 +21938,9 @@ snapshots: - supports-color - utf-8-validate + jsep@1.4.0: + optional: true + jsesc@3.1.0: {} json-bigint@1.0.0: @@ -21647,6 +21978,13 @@ snapshots: jsonparse@1.3.1: {} + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + optional: true + just-diff-apply@5.5.0: {} just-diff@6.0.2: {} @@ -22615,17 +22953,6 @@ snapshots: mkdirp@3.0.1: {} - mlflow-tracing@0.1.3: - dependencies: - '@databricks/sdk-experimental': 0.15.0 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/sdk-node': 0.205.0(@opentelemetry/api@1.9.0) - bignumber.js: 9.3.1 - fast-safe-stringify: 2.1.1 - ini: 5.0.0 - transitivePeerDependencies: - - supports-color - mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -22707,6 +23034,11 @@ snapshots: node-fetch-native@1.6.7: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + optional: true + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -22811,6 +23143,9 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.2 + oauth4webapi@3.8.7: + optional: true + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -22875,6 +23210,12 @@ snapshots: opener@1.5.2: {} + openid-client@6.8.7: + dependencies: + jose: 6.2.10 + oauth4webapi: 3.8.7 + optional: true + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -24244,6 +24585,9 @@ snapshots: reusify@1.1.0: {} + rfc4648@1.5.4: + optional: true + rfdc@1.4.1: {} rimraf@5.0.10: @@ -24807,6 +25151,19 @@ snapshots: stdin-discarder@0.2.2: {} + stream-buffers@3.0.3: + optional: true + + streamx@2.28.1: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + string-argv@0.3.2: {} string-width@4.2.3: @@ -24956,6 +25313,19 @@ snapshots: tar-stream: 2.2.0 optional: true + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.1 + optionalDependencies: + bare-fs: 4.8.1 + bare-path: 3.1.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -24965,6 +25335,18 @@ snapshots: readable-stream: 3.6.2 optional: true + tar-stream@3.2.1: + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.1 + fast-fifo: 1.3.2 + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar@7.5.10: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -24973,6 +25355,14 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -24995,6 +25385,13 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + optional: true + text-extensions@2.4.0: {} thingies@2.5.0(tslib@2.8.1): @@ -25061,6 +25458,9 @@ snapshots: dependencies: tldts: 7.0.17 + tr46@0.0.3: + optional: true + tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -25744,6 +26144,9 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: + optional: true + webidl-conversions@8.0.0: {} webpack-bundle-analyzer@4.10.2(bufferutil@4.0.9): @@ -25890,6 +26293,12 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + optional: true + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7