refactor(eve): remove legacy instrumentation compatibility (#3530)

Signed-off-by: Chad Hietala <chad.hietala@vercel.com>
This commit is contained in:
Chad Hietala
2026-09-18 18:37:52 -04:00
committed by GitHub
parent 8e011906e0
commit 804e670010
16 changed files with 65 additions and 129 deletions
@@ -0,0 +1,5 @@
---
"eve": minor
---
Remove deprecated instrumentation compatibility shapes. Providers now reject the removed `capture` option in favor of `tracePolicy`, destination export policies return object decisions, and flat `instrumentation.ts` modules are no longer discovered.
+2 -15
View File
@@ -119,13 +119,6 @@ export async function discoverAgent(input: DiscoverAgentInput): Promise<Discover
});
diagnostics.push(...configModuleResult.diagnostics);
const instrumentationModuleResult = discoverFlatModuleSource({
rootEntries,
rootPath: agentRoot,
slotName: "instrumentation",
});
diagnostics.push(...instrumentationModuleResult.diagnostics);
const channelsResult = await discoverNamedSourceDirectory({
directoryName: "channels",
invalidDirectoryCode: DISCOVER_CHANNELS_DIRECTORY_INVALID,
@@ -187,19 +180,13 @@ export async function discoverAgent(input: DiscoverAgentInput): Promise<Discover
const instrumentationDirectory = rootEntries.find(
(entry) => entry.name === "instrumentation" && entry.isDirectory(),
);
if (
instrumentationModuleResult.module !== undefined ||
instrumentationDirectory !== undefined
) {
if (instrumentationDirectory !== undefined) {
diagnostics.push(
createDiscoverErrorDiagnostic({
code: DISCOVER_EXTENSION_INSTRUMENTATION_UNSUPPORTED,
message:
"An extension may not declare instrumentation providers — process-wide observability belongs to the consuming agent.",
sourcePath:
instrumentationModuleResult.module === undefined
? join(agentRoot, instrumentationDirectory!.name)
: join(agentRoot, instrumentationModuleResult.module.logicalPath),
sourcePath: join(agentRoot, instrumentationDirectory.name),
}),
);
}
+7 -2
View File
@@ -11697,9 +11697,9 @@ describe("createToolLoopHarness", () => {
const attemptCompleted = vi.fn();
const hooks = createInstrumentationHooks([
{
capture: "content",
events: { "step.attempt.completed": attemptCompleted },
name: "analytics",
tracePolicy: () => ({ emit: true, recordInputs: true, recordOutputs: true }),
},
]);
const runStep = createToolLoopHarness(
@@ -11739,7 +11739,12 @@ describe("createToolLoopHarness", () => {
recordOutputs: true,
tracePolicy: () => ({ emit: true, recordInputs: false, recordOutputs: false }),
});
const hooks = createInstrumentationHooks([{ capture: "content", name: "analytics" }]);
const hooks = createInstrumentationHooks([
{
name: "analytics",
tracePolicy: () => ({ emit: true, recordInputs: true, recordOutputs: true }),
},
]);
const runStep = createToolLoopHarness(
createTestConfig("conversation", undefined, {
instrumentation: bindHookInstrumentation(hooks, undefined, true),
@@ -32,6 +32,8 @@ const traceContext = (audience: "public" | "private" | "unknown" = "unknown") =>
principalType: "anonymous",
});
const contentTracePolicy = () => ({ emit: true, recordInputs: true, recordOutputs: true }) as const;
function createInstrumentationHooks(
...args: Parameters<typeof createUnboundInstrumentationHooks>
): ReturnType<typeof createUnboundInstrumentationHooks> {
@@ -351,7 +353,11 @@ describe("createAiSdkHookBridge", () => {
it("terminalizes started operations when the attempt errors", async () => {
const after = vi.fn();
const hooks = createInstrumentationHooks([
{ capture: "content", events: { "model.call.failed": after }, name: "after" },
{
events: { "model.call.failed": after },
name: "after",
tracePolicy: contentTracePolicy,
},
]);
const bridge = createAiSdkHookBridge(scope, hooks);
@@ -370,7 +376,11 @@ describe("createAiSdkHookBridge", () => {
it("terminalizes started operations with the abort reason", async () => {
const after = vi.fn();
const hooks = createInstrumentationHooks([
{ capture: "content", events: { "tool.call.failed": after }, name: "after" },
{
events: { "tool.call.failed": after },
name: "after",
tracePolicy: contentTracePolicy,
},
]);
const bridge = createAiSdkHookBridge(scope, hooks);
const toolCall = { input: {}, toolCallId: "tool-1", toolName: "search" };
@@ -434,9 +444,9 @@ describe("createAiSdkHookBridge", () => {
});
const hooks = createInstrumentationHooks([
{
capture: "content",
events: { "model.call.completed": after, "model.call.started": before },
name: "spy",
tracePolicy: contentTracePolicy,
},
]);
const bridge = createAiSdkHookBridge(scope, hooks);
@@ -558,13 +568,13 @@ describe("createAiSdkHookBridge", () => {
const actionStarted = vi.fn();
const hooks = createInstrumentationHooks([
{
capture: "content",
events: {
"action.started": actionStarted,
"tool.call.completed": after,
"tool.call.started": before,
},
name: "spy",
tracePolicy: contentTracePolicy,
},
]);
const bridge = createAiSdkHookBridge(scope, hooks);
@@ -664,9 +674,9 @@ describe("createAiSdkHookBridge", () => {
const hooks = createInstrumentationHooks([
{ events: { "tool.call.started": metadataOnly }, name: "metadata-only" },
{
capture: "content",
events: { "tool.call.started": wantsContent },
name: "wants-content",
tracePolicy: contentTracePolicy,
},
]);
const bridge = createAiSdkHookBridge(scope, hooks);
+9 -13
View File
@@ -13,7 +13,7 @@ import {
withoutInstrumentationContent,
} from "#instrumentation/content.js";
import { createLogger, formatError } from "#internal/logging.js";
import { legacyCaptureTracePolicy, resolveTracePolicy } from "#shared/trace-policy.js";
import { resolveTracePolicy } from "#shared/trace-policy.js";
import type { TraceCaptureContext } from "#shared/trace-policy.js";
import type {
@@ -48,18 +48,14 @@ export function createInstrumentationDispatcher(
const decisions = new Map(
providers.map((provider) => [
provider,
resolveTracePolicy(
provider.tracePolicy ?? legacyCaptureTracePolicy(provider.capture),
trace,
(error) => {
if (warnedPolicyFailures.has(provider)) return;
warnedPolicyFailures.add(provider);
log.warn("instrumentation provider trace policy failed", {
error: formatError(error),
provider: provider.name,
});
},
),
resolveTracePolicy(provider.tracePolicy, trace, (error) => {
if (warnedPolicyFailures.has(provider)) return;
warnedPolicyFailures.add(provider);
log.warn("instrumentation provider trace policy failed", {
error: formatError(error),
provider: provider.name,
});
}),
]),
);
const capturesInputs = [...decisions.values()].some(
@@ -1042,18 +1042,6 @@ describe("trace policies", () => {
},
]).capturesContent,
).toBe(false);
expect(
createUnboundInstrumentationHooks([{ capture: "content", name: "legacy" }]).capturesContent,
).toBe(false);
expect(
createUnboundInstrumentationHooks([
{
capture: "content",
name: "explicit-policy",
tracePolicy: () => ({ emit: true, recordInputs: false, recordOutputs: false }),
},
]).capturesContent,
).toBe(false);
expect(
createInstrumentationHooks([{ name: "quiet" }]).forTrace!(traceContext("weather", "private"))
.capturesContent,
@@ -4,11 +4,7 @@ import type { InstrumentationStateSlot } from "#instrumentation/state.js";
import type { RuntimeTraceContext } from "#protocol/message.js";
import type { ChannelAudience } from "#shared/channel-audience.js";
import type { InstrumentationDecision } from "#shared/instrumentation-decision.js";
import type {
InstrumentationCapture,
TraceCaptureContext,
TraceCapturePolicy,
} from "#shared/trace-policy.js";
import type { TraceCaptureContext, TraceCapturePolicy } from "#shared/trace-policy.js";
/**
* Stable eve identity for one model attempt. Retries share `stepIndex` and
@@ -555,11 +551,9 @@ export type InstrumentationEventHandler<TEvent> = (
ctx: InstrumentationHandlerContext,
) => void | PromiseLike<void>;
/** Internal provider shape mirrored by the future public hook contract. */
/** Internal normalized provider shape consumed by the instrumentation bus. */
export interface InstrumentationProviderDefinition {
readonly name: string;
/** @deprecated Use `tracePolicy` to select directional content. */
readonly capture?: InstrumentationCapture;
/** Durable state identity, separate from the human-readable log name. */
readonly stateNamespace?: string;
/** Internal provider-specific projection applied after capture filtering. */
+1 -4
View File
@@ -10,7 +10,7 @@
// from the bus that feeds it.
import type { InstrumentationEvent } from "#instrumentation/lifecycle.js";
import type { JsonValue } from "#shared/json.js";
import type { InstrumentationCapture, TraceCapturePolicy } from "#shared/trace-policy.js";
import type { TraceCapturePolicy } from "#shared/trace-policy.js";
export type { JsonValue } from "#shared/json.js";
@@ -73,7 +73,6 @@ export type {
InstrumentationMemoryRecord,
} from "#instrumentation/memory.js";
export type {
InstrumentationCapture,
TraceCaptureContext,
TraceCapturePolicy,
TracePolicyDecision,
@@ -156,8 +155,6 @@ export type ProviderEvents = {
* completion order.
*/
export interface ProviderDefinition {
/** @deprecated Use `tracePolicy`. Ignored when `tracePolicy` is also set. */
readonly capture?: InstrumentationCapture;
/**
* Whether this provider receives events and which content directions they
* include. Defaults to emitting every audience, with content only for public
@@ -131,6 +131,15 @@ describe("registerInstrumentationProvider", () => {
/The default export of "instrumentation\/otel" is not an instrumentation provider/,
);
});
it("rejects the removed capture option", async () => {
const provider = defineInstrumentation({ capture: "metadata" } as never);
await expect(register("audit", provider)).rejects.toThrow(
/instrumentation\/audit.*no longer supports `capture`.*Use `tracePolicy`/u,
);
expect(getInstrumentationProviders()).toEqual([]);
});
});
describe("seedInstrumentationProviders", () => {
@@ -232,34 +241,6 @@ describe("finalizeInstrumentationProviders", () => {
expect(started.mock.calls[0]?.[0]).toMatchObject({ turnId: "turn-1" });
});
it.each([
["content", true],
["metadata", false],
] as const)(
"maps the deprecated %s capture setting to provider policy",
async (capture, expected) => {
await register("legacy", defineInstrumentation({ capture }));
const runtime = finalizeInstrumentationProviders({ serviceName: "weather-agent" });
expect(runtime.hooks.forTrace?.(traceContext("private")).capturesContent).toBe(expected);
},
);
it("prefers provider tracePolicy over deprecated capture", async () => {
await register(
"provider",
defineInstrumentation({
capture: "metadata",
tracePolicy: () => ({ emit: true, recordInputs: true, recordOutputs: true }),
}),
);
const runtime = finalizeInstrumentationProviders({ serviceName: "weather-agent" });
expect(runtime.hooks.forTrace?.(traceContext("private")).capturesContent).toBe(true);
});
it("still runs execution when no destination was declared", async () => {
// A directory with no `otel()` has nothing to hang a span on, so
// `runInContext` degrades to running the work directly rather than
@@ -86,6 +86,12 @@ export async function registerInstrumentationProvider(input: {
);
}
if (Object.hasOwn(input.value, "capture")) {
throw new Error(
`The instrumentation provider "instrumentation/${input.slot}" no longer supports \`capture\`. Use \`tracePolicy\` to configure content capture.`,
);
}
providerRegistry().set(input.slot, input.value);
await input.value.setup?.(createInstrumentationSetupContext(input.agentName));
}
@@ -146,7 +152,6 @@ function toProviderDefinition(
entry: RegisteredInstrumentationProvider,
): InstrumentationProviderDefinition {
return {
capture: entry.provider.capture,
events: entry.provider.events as InstrumentationProviderDefinition["events"],
flush: entry.provider.flush,
// The file the provider came from, which is the only name an author can
@@ -20,13 +20,13 @@ export interface InstrumentationLayout {
export function resolveInstrumentationLayout(input: {
readonly agentRoot: string;
}): InstrumentationLayout {
const filePath = resolveInstrumentationFile(input.agentRoot);
const removedFilePath = findRemovedInstrumentationFile(input.agentRoot);
const directoryPath = join(input.agentRoot, INSTRUMENTATION_DIRECTORY);
const hasDirectory = existsSync(directoryPath) && statSync(directoryPath).isDirectory();
if (filePath !== undefined) {
if (removedFilePath !== undefined) {
throw new Error(
`Found removed instrumentation file "${filePath}". Move it into the "${INSTRUMENTATION_DIRECTORY}/" directory as one file per provider. See the instrumentation migration guide.`,
`Found removed instrumentation file "${removedFilePath}". Move it into the "${INSTRUMENTATION_DIRECTORY}/" directory as one file per provider. See the instrumentation migration guide.`,
);
}
@@ -77,10 +77,8 @@ function collectInstrumentationProviderModules(
);
}
/**
* Resolves the removed single `agent/instrumentation` module.
*/
function resolveInstrumentationFile(agentRoot: string): string | undefined {
/** Finds the removed single-file layout only so the build can reject it. */
function findRemovedInstrumentationFile(agentRoot: string): string | undefined {
for (const extension of INSTRUMENTATION_EXTENSIONS) {
const candidate = join(agentRoot, `${INSTRUMENTATION_DIRECTORY}${extension}`);
if (existsSync(candidate)) {
@@ -295,7 +295,10 @@ function typeOnlyFixtures(): void {
recordInputs: true,
});
const providerWithCapture: ProviderDefinition = { capture: "content" };
const providerWithCapture: ProviderDefinition = {
// @ts-expect-error Content capture is configured through tracePolicy.
capture: "content",
};
void providerWithCapture;
defineInstrumentation({
-15
View File
@@ -5,9 +5,6 @@ import {
type InstrumentationDecision,
} from "#shared/instrumentation-decision.js";
/** @deprecated Use `TraceCapturePolicy` to select directional content. */
export type InstrumentationCapture = "content" | "metadata";
export type TraceCaptureContext = { readonly agentName: string } & ConversationContext;
export type TracePolicyDecision =
@@ -20,18 +17,6 @@ export type TracePolicyDecision =
export type TraceCapturePolicy = (trace: TraceCaptureContext) => TracePolicyDecision | boolean;
export function legacyCaptureTracePolicy(
capture: InstrumentationCapture | undefined,
): TraceCapturePolicy | undefined {
if (capture === undefined) return undefined;
const recordsContent = capture === "content";
return () => ({
emit: true,
recordInputs: recordsContent,
recordOutputs: recordsContent,
});
}
export function resolveTracePolicy(
policy: TraceCapturePolicy | undefined,
trace: TraceCaptureContext,
@@ -79,19 +79,6 @@ describe("contentFilteringProcessor", () => {
expect(downstream.ended).toEqual([]);
});
it.each([
[true, 1],
[false, 0],
])("preserves the legacy boolean span decision %s", (decision, exported) => {
const downstream = recordingProcessor();
contentFilteringProcessor(downstream, {
span: () => decision,
}).onEnd(span({ "service.name": "weather" }) as never);
expect(downstream.ended).toHaveLength(exported);
});
it("drops the span when a redaction decision names no direction", () => {
const downstream = recordingProcessor();
@@ -263,9 +263,6 @@ function spanExportDecision(
} catch {
return { exported: false, redactInputs: false, redactOutputs: false };
}
if (typeof decision === "boolean") {
return { exported: decision, redactInputs: false, redactOutputs: false };
}
if (typeof decision !== "object" || decision === null) {
return { exported: false, redactInputs: false, redactOutputs: false };
}
@@ -9,8 +9,6 @@ export interface SpanExportContext {
}
export type SpanExportDecision =
/** @deprecated Return `{ emit: boolean }` instead. */
| boolean
| { readonly emit: boolean }
| {
readonly redact: true;