Skip to content

Commit 4699b28

Browse files
jirispilkaclaude
andauthored
feat: Add failure diagnostics to tool-call telemetry (#631)
* feat: add failure diagnostics to tool-call telemetry Tool failures in Mixpanel were blind spots — every failure was FAILED/SOFT_FAIL with no way to tell a user typo from a server crash, making it impossible to prioritize fixes or understand error patterns. New telemetry fields (non-success only): failure_category (INVALID_INPUT | AUTH | INTERNAL_ERROR), failure_http_status, failure_detail (truncated error message), actor_name, and AJV validation diagnostics (keyword, path, missing/additional property). Key decisions: - Three-value enum, not ten — INVALID_INPUT covers both validation and not-found (user-caused); validation_* fields disambiguate when needed - Wire protocol: buildMCPResponse carries transient internal* fields from tool helpers to server dispatch, stripped before reaching MCP clients - actor-mcp is a known gap (opaque isError, defaults to INTERNAL_ERROR) — documented as TODO, deferred to future iteration - Kept server.ts handler structure to minimize structural diff Other changes: - preparePayment → prepareToolCallContext (name was misleading — handles all tool calls, not just paid ones); cleanArgs → toolArgs, logArgs → logSafeArgs, client → apifyClient - dedent for multi-line error messages in server.ts - actor_name extracted via extractActorName() — especially needed for call-actor where the actor slug is only in arguments - INVALID_INPUT propagated across tool helpers (one-line additions per file) - 402 Payment Required now tracked with failure_category instead of empty diagnostics - New helpers: classifyFailureCategory(), extractValidationDiagnostics() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Review comments * fix: improve failure category classification in telemetry * refactor: introduce `ValidationDiagnostics` type for cleaner validation handling Replaced inline Pick definitions for validation diagnostics across multiple files with a shared `ValidationDiagnostics` type in `types.ts`. Simplifies code and ensures consistency while extracting or handling validation-related diagnostics. * refactor: simplify failure diagnostics extraction in server handlers Replaced inline diagnostic extraction logic with `extractToolResponseDiagnostics` utility in `tool_status.ts`. Centralizes diagnostic handling, reduces repetition, and ensures consistent behavior across server responses. * refactor: use constants for HTTP status codes in failure category classification Replaced hardcoded HTTP status codes with constants (`HTTP_UNAUTHORIZED`, `HTTP_FORBIDDEN`, `HTTP_NOT_FOUND`) in `tool_status.ts`. Ensures clarity and reduces potential for errors. Added constant definitions to `const.ts`. * feat: add `actor-mcp` handling and enhance validation diagnostics Added support for extracting `actorId` for `actor-mcp` tools in `extractActorName`. Enhanced validation diagnostics by including error count (`validation_error_count`) and updated related types (`ValidationDiagnostics`, `ToolCallTelemetryProperties`) to ensure consistency. Improved error handling in `extractValidationDiagnostics` to account for multiple validation errors. * refactor: centralize `getToolFullName` and `extractActorName` logic in utilities Removed redundant implementations of actor name and tool full name extraction. Introduced `getToolFullName` and relocated `extractActorName` to `tools.ts` for consistency and reuse. Updated server handlers to use the new utility, simplifying telemetry and tool resolution logic. * test: add unit tests for `getToolFullName` and `extractActorName` functions * fix: reformat failure diagnostics assertion in `extractToolResponseDiagnostics` test * refactor: replace inline failure diagnostics with centralized `telemetry` object Standardized telemetry reporting across tools. Introduced `telemetry` object to replace inline `toolStatus` and failure diagnostics. Updated utility functions to extract AJV error details and telemetry consistently. Refactored server handlers to align with these changes, improving maintainability and consistency in error handling. * fix: include actor details in failure diagnostics for enhanced telemetry logging * refactor: replace `FailureDetails` with `CallDiagnostics` for unified telemetry management Standardized telemetry tracking across the server by replacing `FailureDetails` with the more comprehensive `CallDiagnostics` type. Updated server handlers, utility functions, and tests to consistently use `CallDiagnostics` for handling failure diagnostics and actor-related fields. Improved maintainability and enhanced consistency in telemetry data logging. * fix: enhance failure diagnostics with detailed error information and categories * refactor: clean up CallActorResolvedContext type * fix: simplify isActorMcpServer assignment in call_actor_common.ts --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 537a4d0 commit 4699b28

26 files changed

Lines changed: 931 additions & 324 deletions

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default [
2424
ignores: [
2525
'**/dist', // Build output directory
2626
'**/.venv', // Python virtual environment (if present)
27+
'.claude/worktrees/**', // Local Codex/Claude worktrees are outside this repo's TS project
2728
'evals/*.ts', // Top-level evaluation scripts
2829
'evals/*.md', // Documentation files
2930
'evals/*.json', // Test case data files

src/const.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export const SEGMENT_FLUSH_INTERVAL_MS = 5_000;
190190

191191
// Tool status
192192
/**
193-
* Unified status constants for tool execution lifecycle.
193+
* Unified status constants for the tool execution lifecycle.
194194
* Single source of truth for all tool status values.
195195
*/
196196
export const TOOL_STATUS = {
@@ -200,8 +200,17 @@ export const TOOL_STATUS = {
200200
SOFT_FAIL: 'SOFT_FAIL',
201201
} as const;
202202

203+
export const FAILURE_CATEGORY = {
204+
INVALID_INPUT: 'INVALID_INPUT',
205+
AUTH: 'AUTH',
206+
INTERNAL_ERROR: 'INTERNAL_ERROR',
207+
} as const;
208+
203209
// HTTP status codes
210+
export const HTTP_UNAUTHORIZED = 401;
204211
export const HTTP_PAYMENT_REQUIRED = 402;
212+
export const HTTP_FORBIDDEN = 403;
213+
export const HTTP_NOT_FOUND = 404;
205214

206215
// Modes that allow long running task tool executions
207216
export const ALLOWED_TASK_TOOL_EXECUTION_MODES = ['optional', 'required'] as const;

src/mcp/server.ts

Lines changed: 326 additions & 208 deletions
Large diffs are not rendered by default.

src/payments/helpers.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,72 @@ import { buildPaymentRequiredResponse, registerPaymentRequiredInterceptor } from
44
import type { PaymentMeta, PaymentProvider, RequestHeaders } from './types.js';
55

66
/**
7-
* Result of preparing payment context for a tool call.
8-
* Centralizes all payment-related processing into a single step.
7+
* Result of preparing tool-call context.
8+
* Centralizes payment-aware argument sanitization, logging data, and client creation.
99
*/
10-
export type PreparePaymentResult = {
10+
export type PrepareToolCallContextResult = {
1111
/** Structured error result for a 402 PaymentRequired response. Undefined if no error. */
12-
errorResult?: ReturnType<typeof buildPaymentRequiredResponse>;
13-
/** Args with payment-specific fields removed — safe for ajv validation and Actor input. */
14-
cleanArgs: Record<string, unknown>;
15-
/** Args with sensitive payment fields redacted — safe for logging. */
16-
logArgs: unknown;
12+
paymentRequiredResult?: ReturnType<typeof buildPaymentRequiredResponse>;
13+
/** Tool args with payment-specific fields removed — safe for ajv validation and Actor input. */
14+
toolArgs: Record<string, unknown>;
15+
/** Tool args with sensitive payment fields redacted — safe for logging. */
16+
logSafeArgs: unknown;
1717
/** ApifyClient configured with payment headers (if applicable) or standard token. */
18-
client: ApifyClient;
18+
apifyClient: ApifyClient;
1919
};
2020

2121
/**
22-
* Prepares payment context for a tool call.
22+
* Prepares tool-call context before validation and execution.
2323
*
2424
* This helper centralizes all payment processing:
2525
* 1. Validates payment credentials (for tools with `paymentRequired: true`)
2626
* 2. Strips payment fields from args (for clean ajv validation and Actor input)
2727
* 3. Redacts sensitive fields for logging
2828
* 4. Creates an ApifyClient with payment headers or standard token
2929
*
30-
* Call this BEFORE ajv validation so `cleanArgs` can be validated without
30+
* Call this BEFORE ajv validation so `toolArgs` can be validated without
3131
* the `additionalProperties: true` hack.
32+
*
33+
* TODO: This function has mixed responsibilities. It should be split into separate concerns:
34+
* - validatePaymentCredentials (returns error or void)
35+
* - sanitizeToolArgs (returns toolArgs and logSafeArgs)
36+
* - createApifyClient (returns apifyClient)
3237
*/
33-
export function preparePayment(input: {
38+
export function prepareToolCallContext(input: {
3439
provider: PaymentProvider | undefined;
3540
tool: ToolEntry;
3641
args: Record<string, unknown>;
3742
apifyToken: ApifyToken;
3843
meta?: PaymentMeta;
3944
requestHeaders?: RequestHeaders;
40-
}): PreparePaymentResult {
45+
}): PrepareToolCallContextResult {
4146
const { provider, tool, args, apifyToken, meta, requestHeaders } = input;
4247

4348
if (!provider) {
44-
const client = new ApifyClient({ token: apifyToken });
45-
registerPaymentRequiredInterceptor(client);
49+
const apifyClient = new ApifyClient({ token: apifyToken });
50+
registerPaymentRequiredInterceptor(apifyClient);
4651
return {
47-
cleanArgs: args,
48-
logArgs: args,
49-
client,
52+
toolArgs: args,
53+
logSafeArgs: args,
54+
apifyClient,
5055
};
5156
}
5257

5358
const error = tool.paymentRequired ? provider.validatePayment(args, meta, requestHeaders) : null;
5459
const errorData = error && provider.getPaymentRequiredData ? provider.getPaymentRequiredData() : undefined;
55-
const cleanArgs = provider.removePaymentFields(args);
56-
const logArgs = provider.redactForLogging(args);
60+
const toolArgs = provider.removePaymentFields(args);
61+
const logSafeArgs = provider.redactForLogging(args);
5762

5863
const paymentHeaders = provider.getPaymentHeaders(args, meta, requestHeaders);
59-
const client = Object.keys(paymentHeaders).length > 0
64+
const apifyClient = Object.keys(paymentHeaders).length > 0
6065
? new ApifyClient({ paymentHeaders })
6166
: new ApifyClient({ token: apifyToken });
62-
registerPaymentRequiredInterceptor(client);
67+
registerPaymentRequiredInterceptor(apifyClient);
6368

6469
return {
65-
errorResult: error ? buildPaymentRequiredResponse(error, errorData) : undefined,
66-
cleanArgs,
67-
logArgs,
68-
client,
70+
paymentRequiredResult: error ? buildPaymentRequiredResponse(error, errorData) : undefined,
71+
toolArgs,
72+
logSafeArgs,
73+
apifyClient,
6974
};
7075
}

src/payments/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export type { PaymentHeaders, PaymentMeta, PaymentProvider, PaymentProviderId, R
22
export { SkyfirePaymentProvider } from './skyfire.js';
33
export { X402PaymentProvider } from './x402.js';
44
export { resolvePaymentProvider } from './resolve.js';
5-
export { preparePayment } from './helpers.js';
6-
export type { PreparePaymentResult } from './helpers.js';
5+
export { prepareToolCallContext } from './helpers.js';
6+
export type { PrepareToolCallContextResult } from './helpers.js';

src/tools/common/fetch_apify_docs.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { z } from 'zod';
22

33
import log from '@apify/log';
44

5-
import { ALLOWED_DOC_DOMAINS, HelperTools, TOOL_STATUS } from '../../const.js';
5+
import { ALLOWED_DOC_DOMAINS, FAILURE_CATEGORY, HelperTools, TOOL_STATUS } from '../../const.js';
66
import { fetchApifyDocsCache } from '../../state.js';
77
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../../types.js';
88
import { compileSchema } from '../../utils/ajv.js';
99
import { logHttpError } from '../../utils/logging.js';
1010
import { buildMCPResponse } from '../../utils/mcp.js';
11+
import { classifyFailureCategory } from '../../utils/tool_status.js';
1112
import { fetchApifyDocsToolOutputSchema } from '../structured_output_schemas.js';
1213

1314
const fetchApifyDocsToolArgsSchema = z.object({
@@ -65,7 +66,7 @@ Only documentation URLs from Apify and Crawlee are allowed \
6566
Please provide a valid documentation URL. \
6667
You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`],
6768
isError: true,
68-
toolStatus: TOOL_STATUS.SOFT_FAIL,
69+
telemetry: { toolStatus: TOOL_STATUS.SOFT_FAIL, failureCategory: FAILURE_CATEGORY.INVALID_INPUT },
6970
});
7071
}
7172

@@ -87,7 +88,11 @@ You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`],
8788
return buildMCPResponse({
8889
texts: [buildFetchErrorMessage(url, `HTTP Status: ${response.status} ${response.statusText}.`)],
8990
isError: true,
90-
toolStatus: isUserError ? TOOL_STATUS.SOFT_FAIL : TOOL_STATUS.FAILED,
91+
telemetry: {
92+
toolStatus: isUserError ? TOOL_STATUS.SOFT_FAIL : TOOL_STATUS.FAILED,
93+
failureCategory: classifyFailureCategory(error),
94+
failureHttpStatus: response.status,
95+
},
9196
});
9297
}
9398
markdown = await response.text();
@@ -97,7 +102,7 @@ You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`],
97102
return buildMCPResponse({
98103
texts: [buildFetchErrorMessage(url, `Error: ${error instanceof Error ? error.message : String(error)}.`)],
99104
isError: true,
100-
toolStatus: TOOL_STATUS.SOFT_FAIL,
105+
telemetry: { toolStatus: TOOL_STATUS.SOFT_FAIL, failureCategory: FAILURE_CATEGORY.INTERNAL_ERROR },
101106
});
102107
}
103108
}

src/tools/common/get_actor_output.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dedent from 'dedent';
22
import { z } from 'zod';
33

4-
import { HelperTools, TOOL_MAX_OUTPUT_CHARS, TOOL_STATUS } from '../../const.js';
4+
import { FAILURE_CATEGORY, HelperTools, TOOL_MAX_OUTPUT_CHARS, TOOL_STATUS } from '../../const.js';
55
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../../types.js';
66
import { compileSchema } from '../../utils/ajv.js';
77
import { getValuesByDotKeys, parseCommaSeparatedList } from '../../utils/generic.js';
@@ -120,7 +120,7 @@ export const getActorOutput: ToolEntry = Object.freeze({
120120
return buildMCPResponse({
121121
texts: [`Dataset '${parsed.datasetId}' not found.`],
122122
isError: true,
123-
toolStatus: TOOL_STATUS.SOFT_FAIL,
123+
telemetry: { toolStatus: TOOL_STATUS.SOFT_FAIL, failureCategory: FAILURE_CATEGORY.INVALID_INPUT },
124124
});
125125
}
126126

src/tools/common/get_dataset.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from 'zod';
22

3-
import { HelperTools, TOOL_STATUS } from '../../const.js';
3+
import { FAILURE_CATEGORY, HelperTools, TOOL_STATUS } from '../../const.js';
44
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../../types.js';
55
import { compileSchema } from '../../utils/ajv.js';
66
import { buildMCPResponse } from '../../utils/mcp.js';
@@ -46,7 +46,7 @@ USAGE EXAMPLES:
4646
return buildMCPResponse({
4747
texts: [`Dataset '${parsed.datasetId}' not found.`],
4848
isError: true,
49-
toolStatus: TOOL_STATUS.SOFT_FAIL,
49+
telemetry: { toolStatus: TOOL_STATUS.SOFT_FAIL, failureCategory: FAILURE_CATEGORY.INVALID_INPUT },
5050
});
5151
}
5252
return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] };

src/tools/common/get_dataset_items.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const getDatasetItems: ToolEntry = Object.freeze({
8484
return buildMCPResponse({
8585
texts: [`Dataset '${parsed.datasetId}' not found.`],
8686
isError: true,
87-
toolStatus: TOOL_STATUS.SOFT_FAIL,
87+
telemetry: { toolStatus: TOOL_STATUS.SOFT_FAIL },
8888
});
8989
}
9090

src/tools/common/get_dataset_schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ USAGE EXAMPLES:
6161
return buildMCPResponse({
6262
texts: [`Dataset '${parsed.datasetId}' not found.`],
6363
isError: true,
64-
toolStatus: TOOL_STATUS.SOFT_FAIL,
64+
telemetry: { toolStatus: TOOL_STATUS.SOFT_FAIL },
6565
});
6666
}
6767

@@ -83,7 +83,7 @@ USAGE EXAMPLES:
8383
return buildMCPResponse({
8484
texts: [`Failed to generate schema for dataset '${parsed.datasetId}'.`],
8585
isError: true,
86-
toolStatus: TOOL_STATUS.FAILED,
86+
telemetry: { toolStatus: TOOL_STATUS.FAILED },
8787
});
8888
}
8989

0 commit comments

Comments
 (0)