mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
refactor(eve): remove legacy instrumentation compatibility (#3530)
Signed-off-by: Chad Hietala <chad.hietala@vercel.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user