From 9ef270d4b6e864380232b6f1581bbf3b82bf06fb Mon Sep 17 00:00:00 2001 From: nikkikapadia Date: Wed, 7 Oct 2026 14:19:25 -0400 Subject: [PATCH 1/3] feat(search-events): correctly process attribute context in tests + deprecated attribute evals --- packages/mcp-core/src/api-client/client.ts | 30 ++- .../src/tools/support/search-events/config.ts | 1 + .../tools/support/search-events/utils.test.ts | 210 +++++++++++++++++- .../src/tools/support/search-events/utils.ts | 122 ++++++++-- ...rch-events-agent-attribute-context.eval.ts | 13 +- 5 files changed, 346 insertions(+), 30 deletions(-) diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index b8d7a0004..3e7737374 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -349,12 +349,22 @@ export type TraceItemAttributeSource = { is_transformed_alias?: boolean; }; +export type TraceItemAttributeContext = { + isConvention?: boolean; + brief?: string; + isDeprecated?: boolean; + details?: string[]; + examples?: Array; + replacementAttribute?: string; +}; + export type TraceItemAttribute = { key: string; name: string; type: TraceItemAttributeType; attributeSource: TraceItemAttributeSource; secondaryAliases?: string[]; + context?: TraceItemAttributeContext; }; export type TraceItemAttributeValidationResult = { @@ -474,6 +484,15 @@ const EventsValidationResponseSchema = z }) .transform((result): EventsValidationResult => result); +const TraceItemAttributeContextSchema = z.object({ + isConvention: z.boolean().optional(), + brief: z.string().optional(), + isDeprecated: z.boolean().optional(), + examples: z.array(z.union([z.string(), z.boolean(), z.number()])).optional(), + replacementAttribute: z.string().optional(), + details: z.array(z.string()).optional(), +}); + const TraceItemAttributeSchema = z .object({ key: z.string(), @@ -481,14 +500,23 @@ const TraceItemAttributeSchema = z attributeType: TraceItemAttributeTypeSchema, attributeSource: TraceItemAttributeSourceSchema, secondaryAliases: z.array(z.string()).optional(), + context: TraceItemAttributeContextSchema.optional(), }) .transform( - ({ key, name, attributeType, attributeSource, secondaryAliases }) => ({ + ({ + key, + name, + attributeType, + attributeSource, + secondaryAliases, + context, + }) => ({ key, name, type: attributeType, attributeSource, ...(secondaryAliases ? { secondaryAliases } : {}), + context, }), ); diff --git a/packages/mcp-core/src/tools/support/search-events/config.ts b/packages/mcp-core/src/tools/support/search-events/config.ts index 27d4a42a4..bb09b13c2 100644 --- a/packages/mcp-core/src/tools/support/search-events/config.ts +++ b/packages/mcp-core/src/tools/support/search-events/config.ts @@ -43,6 +43,7 @@ TOOL USAGE GUIDELINES: 7. Use datasetAttributes substringMatch, query, and attributeTypes for targeted lookup when broad field discovery is truncated 8. For non-replay datasets, call validateSearch after constructing the candidate request. If invalid, fix and validate again in this same pass 9. NEVER replace a structured field:value filter with message/log.body/full-text matching. If an explicit field is unavailable on the dataset, keep it and let validation fail instead of inventing a weaker query +10. If datasetAttributes lists a field under Deprecated Fields, use its replacement instead, even when the deprecated name appears in the guidance or examples in this prompt CRITICAL - TOOL RESPONSE HANDLING: All tools return responses in this format: {error?: string, result?: data} diff --git a/packages/mcp-core/src/tools/support/search-events/utils.test.ts b/packages/mcp-core/src/tools/support/search-events/utils.test.ts index 4dbb0c158..e7e84857b 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.test.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.test.ts @@ -585,8 +585,24 @@ describe("fetchCustomAttributes", () => { expect(result).toEqual({ attributes: { - "span.op": "Operation", - "span.duration": "Duration", + "span.duration": { + attributeSource: { + source_type: "sentry", + }, + context: undefined, + key: "span.duration", + name: "Duration", + type: "number", + }, + "span.op": { + attributeSource: { + source_type: "sentry", + }, + context: undefined, + key: "span.op", + name: "Operation", + type: "string", + }, }, fieldTypes: { "span.op": "string", @@ -614,8 +630,12 @@ describe("fetchCustomAttributes", () => { expect(result).toEqual({ attributes: { - browser: "Browser", - environment: "Environment", + browser: { key: "browser", name: "Browser", totalValues: 10 }, + environment: { + key: "environment", + name: "Environment", + totalValues: 3, + }, }, fieldTypes: {}, }); @@ -663,9 +683,33 @@ describe("fetchCustomAttributes", () => { expect(result).toEqual({ attributes: { - "metric.name": "Metric Name", - "metric.type": "Metric Type", - value: "Metric Value", + "metric.name": { + attributeSource: { + source_type: "sentry", + }, + context: undefined, + key: "metric.name", + name: "Metric Name", + type: "string", + }, + "metric.type": { + attributeSource: { + source_type: "sentry", + }, + context: undefined, + key: "metric.type", + name: "Metric Type", + type: "string", + }, + value: { + attributeSource: { + source_type: "sentry", + }, + context: undefined, + key: "value", + name: "Metric Value", + type: "number", + }, }, fieldTypes: { "metric.name": "string", @@ -733,9 +777,33 @@ describe("fetchCustomAttributes", () => { } expect(result).toEqual({ attributes: { - "tags[type]": "type", - "tags[sequence,number]": "sequence", - "tags[enabled,boolean]": "enabled", + "tags[enabled,boolean]": { + attributeSource: { + source_type: "user", + }, + context: undefined, + key: "tags[enabled,boolean]", + name: "enabled", + type: "boolean", + }, + "tags[sequence,number]": { + attributeSource: { + source_type: "user", + }, + context: undefined, + key: "tags[sequence,number]", + name: "sequence", + type: "number", + }, + "tags[type]": { + attributeSource: { + source_type: "sentry", + }, + context: undefined, + key: "tags[type]", + name: "type", + type: "string", + }, }, fieldTypes: { "tags[type]": "string", @@ -744,6 +812,128 @@ describe("fetchCustomAttributes", () => { }, }); }); + + it("should properly digest and return context when received from the attributes endpoint", async () => { + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/test-org/trace-items/attributes/?expand=context", + ({ request }) => { + const url = new URL(request.url); + const itemType = url.searchParams.get("itemType"); + + if (!itemType) { + return HttpResponse.json( + { detail: "Missing required parameters" }, + { status: 400 }, + ); + } + + return HttpResponse.json([ + { + key: "span.op", + name: "Operation", + attributeType: "string", + attributeSource: { source_type: "sentry" }, + context: { + isConvention: true, + isDeprecated: false, + brief: "The operation of a span", + examples: ["http.client"], + }, + }, + { + key: "sentry:internal", + name: "Internal", + attributeType: "string", + attributeSource: { source_type: "sentry" }, + context: {}, + }, + { + key: "span.duration", + name: "Duration", + attributeType: "number", + attributeSource: { source_type: "sentry" }, + context: { + isConvention: false, + brief: "The total time taken by the span.", + isDeprecated: false, + }, + }, + { + key: "tags[app_start_type,string]", + name: "app_start_type", + attributeSource: { + source_type: "user", + }, + attributeType: "string", + context: { + isConvention: true, + brief: "Mobile app start variant. Either cold or warm.", + isDeprecated: true, + examples: ["cold"], + replacementAttribute: "app.vitals.start.type", + }, + }, + ]); + }, + ), + ); + + const result = await fetchCustomAttributes( + apiService, + "test-org", + "spans", + undefined, + undefined, + { context: true }, + ); + + expect(result).toEqual({ + attributes: { + "span.op": { + key: "span.op", + name: "Operation", + type: "string", + attributeSource: { source_type: "sentry" }, + context: { + isConvention: true, + isDeprecated: false, + brief: "The operation of a span", + examples: ["http.client"], + }, + }, + "span.duration": { + key: "span.duration", + name: "Duration", + type: "number", + attributeSource: { source_type: "sentry" }, + context: { + isConvention: false, + brief: "The total time taken by the span.", + isDeprecated: false, + }, + }, + "tags[app_start_type,string]": { + key: "tags[app_start_type,string]", + name: "app_start_type", + type: "string", + attributeSource: { source_type: "user" }, + context: { + isConvention: true, + brief: "Mobile app start variant. Either cold or warm.", + isDeprecated: true, + examples: ["cold"], + replacementAttribute: "app.vitals.start.type", + }, + }, + }, + fieldTypes: { + "span.op": "string", + "span.duration": "number", + "tags[app_start_type,string]": "string", + }, + }); + }); }); }); diff --git a/packages/mcp-core/src/tools/support/search-events/utils.ts b/packages/mcp-core/src/tools/support/search-events/utils.ts index 6641fdfe1..d2b0e4a9e 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.ts @@ -4,6 +4,9 @@ import type { EventsQueryValidation, EventsValidationResult, SentryApiService, + Tag, + TraceItemAttribute, + TraceItemAttributeContext, TraceItemAttributeType, TraceItemType, } from "../../../api-client"; @@ -593,10 +596,10 @@ export async function fetchCustomAttributes( context?: boolean; } = {}, ): Promise<{ - attributes: Record; + attributes: Record; fieldTypes: Record; }> { - const customAttributes: Record = {}; + const customAttributes: Record = {}; const fieldTypes: Record = {}; const normalizedDataset = normalizeEventsDataset(dataset); const attributeTimeParams = timeParams ?? { statsPeriod: "14d" }; @@ -617,7 +620,7 @@ export async function fetchCustomAttributes( for (const tag of tagsResponse) { if (tag.key && !tag.key.startsWith("sentry:")) { - customAttributes[tag.key] = tag.name || tag.key; + customAttributes[tag.key] = { ...tag, name: tag.name || tag.key }; } } } else if (normalizedDataset === "profiles") { @@ -646,7 +649,7 @@ export async function fetchCustomAttributes( for (const attr of attributesResponse) { if (attr.key && !attr.key.startsWith("sentry:")) { - customAttributes[attr.key] = attr.name || attr.key; + customAttributes[attr.key] = { ...attr, name: attr.name || attr.key }; // Track field type from the attribute response with validation if (attr.type) { fieldTypes[attr.key] = attr.type; @@ -941,6 +944,82 @@ export async function assertEventsSearchIsValid( } } +/** + * Describes an attribute the agent can query, from either the static field config or the + * attributes API. `context` carries semantic convention details when available. + */ +type AttributeDescriptor = { + key: string; + name: string; + context?: TraceItemAttributeContext; +}; + +/** + * Convert static `{ key: description }` field config into the same + * `{ key: { key, name } }` shape returned by fetchCustomAttributes + */ +function toAttributeRecord( + fields: Record, +): Record { + return Object.fromEntries( + Object.entries(fields).map(([key, name]) => [key, { key, name }]), + ); +} + +/** + * Swap each deprecated field for its replacement when the replacement is also + * available. The replacement takes the deprecated field's slot so it isn't + * truncated off the end of the listing, and the swapped-out fields are + * returned separately so the agent can be told not to use them. + */ +function preferReplacementFields(fields: Record): { + available: Map; + replaced: AttributeDescriptor[]; +} { + const available = new Map(); + const replaced: AttributeDescriptor[] = []; + + for (const [key, field] of Object.entries(fields)) { + const replacementKey = field.context?.isDeprecated + ? field.context.replacementAttribute + : undefined; + const replacement = replacementKey ? fields[replacementKey] : undefined; + + if (replacementKey && replacement) { + replaced.push(field); + if (!available.has(replacementKey)) { + available.set(replacementKey, replacement); + } + } else if (!available.has(key)) { + available.set(key, field); + } + } + + return { available, replaced }; +} + +/** + * Describe a field for the agent, leading with deprecation so it isn't missed + */ +function describeField(field: AttributeDescriptor): string { + const { context } = field; + const parts: string[] = []; + + if (context?.isDeprecated) { + parts.push( + context.replacementAttribute + ? `DEPRECATED: use ${context.replacementAttribute} instead.` + : "DEPRECATED.", + ); + } + parts.push(context?.brief || field.name); + if (context?.examples?.length) { + parts.push(`Examples: ${context.examples.join(", ")}.`); + } + + return parts.join(" "); +} + /** * Create a tool for the agent to query available attributes by dataset * The tool is pre-bound with the API service and organization configured for the appropriate region @@ -1012,12 +1091,16 @@ export function createDatasetAttributesTool(options: { ); // Combine all available fields - const allFields = { - ...BASE_COMMON_FIELDS, - ...DATASET_FIELDS[normalizedDataset], + const allFields: Record = { + ...toAttributeRecord(BASE_COMMON_FIELDS), + ...toAttributeRecord(DATASET_FIELDS[normalizedDataset]), ...customAttributes, }; - const fieldCount = Object.keys(allFields).length; + + const { available: availableFields, replaced: replacedFields } = + preferReplacementFields(allFields); + const replacedKeys = new Set(replacedFields.map((field) => field.key)); + const fieldCount = availableFields.size; const recommendedFields = RECOMMENDED_FIELDS[normalizedDataset]; @@ -1030,27 +1113,40 @@ export function createDatasetAttributesTool(options: { for (const field of staticNumericFields) { allFieldTypes[field] = "number"; } + const fieldTypeEntries = Object.entries(allFieldTypes).filter( + ([key]) => !replacedKeys.has(key), + ); recordAgentToolResultCount(fieldCount); return `Dataset: ${dataset} Available Fields (${fieldCount} total): -${Object.entries(allFields) +${[...availableFields] .slice(0, 50) // Limit to first 50 to avoid overwhelming the agent - .map(([key, desc]) => `- ${key}: ${desc}`) + .map(([key, field]) => `- ${key}: ${describeField(field)}`) .join("\n")} ${fieldCount > 50 ? `\n... and ${fieldCount - 50} more fields` : ""} - +${ + replacedFields.length > 0 + ? ` +Deprecated Fields (do NOT use these, use the replacement instead): +${replacedFields + .slice(0, 30) + .map((field) => `- ${field.key} → ${field.context?.replacementAttribute}`) + .join("\n")} +${replacedFields.length > 30 ? `\n... and ${replacedFields.length - 30} more deprecated fields` : ""}` + : "" +} Recommended Fields for ${dataset}: ${recommendedFields.basic.map((f) => `- ${f}`).join("\n")} Field Types (CRITICAL for aggregate functions): -${Object.entries(allFieldTypes) +${fieldTypeEntries .slice(0, 30) // Show more field types since this is critical for aggregate functions .map(([key, type]) => `- ${key}: ${type}`) .join("\n")} -${Object.keys(allFieldTypes).length > 30 ? `\n... and ${Object.keys(allFieldTypes).length - 30} more fields` : ""} +${fieldTypeEntries.length > 30 ? `\n... and ${fieldTypeEntries.length - 30} more fields` : ""} IMPORTANT: Only use numeric aggregate functions (avg, sum, min, max, percentiles) with numeric fields. Use count() or count_unique() for non-numeric fields. diff --git a/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts b/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts index 85ae9fbff..0ca12f1f3 100644 --- a/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts +++ b/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts @@ -38,6 +38,7 @@ describeEval("search-events-agent-attributes-without-context", { name: "datasetAttributes", arguments: { dataset: "spans", + substringMatch: "http.method", }, }, ], @@ -99,7 +100,7 @@ describeEval("search-events-agent-attributes-with-context", { data: async () => { return [ { - // EVENTUALLY Context marks http.method as deprecated in favor of http.request.method + // Context marks http.method as deprecated in favor of http.request.method input: "Count spans grouped by HTTP method over the last 7 days", expectedTools: [ { @@ -113,12 +114,12 @@ describeEval("search-events-agent-attributes-with-context", { dataset: "spans", fields: (value: unknown) => Array.isArray(value) && - !value.includes("http.request.method") && - value.includes("http.method"), + value.includes("http.request.method") && + !value.includes("http.method"), }, }, { - // EVENTUALLY Context marks http.status_code as deprecated in favor of + // Context marks http.status_code as deprecated in favor of // http.response.status_code input: "Show me spans with HTTP status code 503 in the last 24 hours", expectedTools: [ @@ -133,8 +134,8 @@ describeEval("search-events-agent-attributes-with-context", { dataset: "spans", query: (value: unknown) => typeof value === "string" && - !value.includes("http.response.status_code:503") && - value.includes("http.status_code"), + value.includes("http.response.status_code:503") && + !value.includes("http.status_code"), }, }, ]; From 1223591b3d19f740284b5be38185a840cb7756ea Mon Sep 17 00:00:00 2001 From: nikkikapadia Date: Fri, 9 Oct 2026 11:57:02 -0400 Subject: [PATCH 2/3] address feedback, add more evals, adjust mocks and prompts --- packages/mcp-core/src/api-client/client.ts | 2 +- .../src/tools/support/search-events/config.ts | 61 ++++++++++++- .../src/tools/support/search-events/utils.ts | 40 ++++---- ...rch-events-agent-attribute-context.eval.ts | 91 ++++++++++++++++++- ...-attributes-spans-number-with-context.json | 10 -- ...-attributes-spans-string-with-context.json | 12 --- 6 files changed, 164 insertions(+), 52 deletions(-) diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 3e7737374..fdd5d8f72 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -515,7 +515,7 @@ const TraceItemAttributeSchema = z name, type: attributeType, attributeSource, - ...(secondaryAliases ? { secondaryAliases } : {}), + ...(secondaryAliases && { secondaryAliases }), context, }), ); diff --git a/packages/mcp-core/src/tools/support/search-events/config.ts b/packages/mcp-core/src/tools/support/search-events/config.ts index bb09b13c2..dab182818 100644 --- a/packages/mcp-core/src/tools/support/search-events/config.ts +++ b/packages/mcp-core/src/tools/support/search-events/config.ts @@ -40,10 +40,10 @@ TOOL USAGE GUIDELINES: 4. Use whoami tool when queries contain "me" references for user.id or user.email fields 5. IMPORTANT: For ambiguous terms like "user agents", "browser", "client" - use the appropriate field discovery tool instead of guessing field names 6. When the user already supplied Sentry search syntax for spans/logs/metrics, call datasetAttributes with substringMatch or query filters from the request before dropping or renaming fields -7. Use datasetAttributes substringMatch, query, and attributeTypes for targeted lookup when broad field discovery is truncated -8. For non-replay datasets, call validateSearch after constructing the candidate request. If invalid, fix and validate again in this same pass -9. NEVER replace a structured field:value filter with message/log.body/full-text matching. If an explicit field is unavailable on the dataset, keep it and let validation fail instead of inventing a weaker query -10. If datasetAttributes lists a field under Deprecated Fields, use its replacement instead, even when the deprecated name appears in the guidance or examples in this prompt +7. If datasetAttributes lists a field under Deprecated Fields, use its replacement instead, even when the deprecated name appears in the guidance or examples in this prompt +8. Use datasetAttributes substringMatch, query, and attributeTypes for targeted lookup when broad field discovery is truncated +9. For non-replay datasets, call validateSearch after constructing the candidate request. If invalid, fix and validate again in this same pass +10. NEVER replace a structured field:value filter with message/log.body/full-text matching. If an explicit field is unavailable on the dataset, keep it and let validation fail instead of inventing a weaker query CRITICAL - TOOL RESPONSE HANDLING: All tools return responses in this format: {error?: string, result?: data} @@ -233,6 +233,7 @@ Return a JSON object with these fields: CORRECT QUERY PATTERNS (FOLLOW THESE): - For field existence: Use has:field_name (NOT field_name IS NOT NULL) - For field absence: Use !has:field_name (NOT field_name IS NULL) +- For field negation: Use !span.op:db (NOT span.op IS db) - For time periods: Use timeRange parameter (NOT SQL date functions) - Example: "items processed yesterday" → query: "has:item.processed", timeRange: {"statsPeriod": "24h"} @@ -622,6 +623,21 @@ export const DATASET_EXAMPLES: Record< sort: "-p75(span.duration)", }, }, + { + description: "spans that did not succeed, excluding database queries", + output: { + query: "!span.status:ok AND !span.op:db*", + fields: [ + "span.op", + "span.description", + "span.status", + "span.duration", + "transaction", + "timestamp", + ], + sort: "-span.duration", + }, + }, ], errors: [ { @@ -679,6 +695,14 @@ export const DATASET_EXAMPLES: Record< sort: "-count()", }, }, + { + description: "errors excluding TypeError and ReferenceError", + output: { + query: "level:error AND !error.type:[TypeError,ReferenceError]", + fields: ["error.type", "count()"], + sort: "-count()", + }, + }, ], logs: [ { @@ -721,6 +745,14 @@ export const DATASET_EXAMPLES: Record< sort: "-timestamp", }, }, + { + description: "error logs excluding health checks", + output: { + query: 'severity:error AND !message:"*health check*"', + fields: ["timestamp", "message", "severity", "trace"], + sort: "-timestamp", + }, + }, ], tracemetrics: [ { @@ -787,6 +819,19 @@ export const DATASET_EXAMPLES: Record< sort: "-avg(value,http.request.duration,distribution,millisecond)", }, }, + { + description: "request duration by route, excluding health check routes", + output: { + query: + "metric.name:http.request.duration AND metric.type:distribution AND !transaction:*health*", + fields: [ + "transaction", + "p95(value,http.request.duration,distribution,millisecond)", + "count(value,http.request.duration,distribution,millisecond)", + ], + sort: "-p95(value,http.request.duration,distribution,millisecond)", + }, + }, ], profiles: [ { @@ -845,6 +890,14 @@ export const DATASET_EXAMPLES: Record< sort: "-count()", }, }, + { + description: "slowest profiled transactions outside development", + output: { + query: "!environment:development", + fields: ["transaction", "p95()", "count()"], + sort: "-p95()", + }, + }, ], }; diff --git a/packages/mcp-core/src/tools/support/search-events/utils.ts b/packages/mcp-core/src/tools/support/search-events/utils.ts index d2b0e4a9e..abd7aa324 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.ts @@ -967,12 +967,10 @@ function toAttributeRecord( } /** - * Swap each deprecated field for its replacement when the replacement is also - * available. The replacement takes the deprecated field's slot so it isn't - * truncated off the end of the listing, and the swapped-out fields are - * returned separately so the agent can be told not to use them. + * Swap each deprecated key for its replacement key when available, + * otherwise remove the deprecated attribute from the available attributes. */ -function preferReplacementFields(fields: Record): { +function resolveAvailableFields(fields: Record): { available: Map; replaced: AttributeDescriptor[]; } { @@ -980,17 +978,20 @@ function preferReplacementFields(fields: Record): { const replaced: AttributeDescriptor[] = []; for (const [key, field] of Object.entries(fields)) { - const replacementKey = field.context?.isDeprecated - ? field.context.replacementAttribute - : undefined; - const replacement = replacementKey ? fields[replacementKey] : undefined; - - if (replacementKey && replacement) { + const attributeDeprecated = field.context?.isDeprecated; + const replacementKey = + attributeDeprecated && field.context + ? field.context.replacementAttribute + : undefined; + if (attributeDeprecated && !replacementKey) { + continue; + } else if (replacementKey) { + const replacementAttribute = { ...field, key: replacementKey }; + available.set(replacementKey, replacementAttribute); replaced.push(field); - if (!available.has(replacementKey)) { - available.set(replacementKey, replacement); - } - } else if (!available.has(key)) { + } + + if (!available.has(key)) { available.set(key, field); } } @@ -1005,13 +1006,6 @@ function describeField(field: AttributeDescriptor): string { const { context } = field; const parts: string[] = []; - if (context?.isDeprecated) { - parts.push( - context.replacementAttribute - ? `DEPRECATED: use ${context.replacementAttribute} instead.` - : "DEPRECATED.", - ); - } parts.push(context?.brief || field.name); if (context?.examples?.length) { parts.push(`Examples: ${context.examples.join(", ")}.`); @@ -1098,7 +1092,7 @@ export function createDatasetAttributesTool(options: { }; const { available: availableFields, replaced: replacedFields } = - preferReplacementFields(allFields); + resolveAvailableFields(allFields); const replacedKeys = new Set(replacedFields.map((field) => field.key)); const fieldCount = availableFields.size; diff --git a/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts b/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts index 0ca12f1f3..9adbb774a 100644 --- a/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts +++ b/packages/mcp-server-evals/src/evals/search-events-agent-attribute-context.eval.ts @@ -38,7 +38,6 @@ describeEval("search-events-agent-attributes-without-context", { name: "datasetAttributes", arguments: { dataset: "spans", - substringMatch: "http.method", }, }, ], @@ -69,6 +68,45 @@ describeEval("search-events-agent-attributes-without-context", { !value.includes("http.response.status_code"), }, }, + { + // transaction is picked from span fields + input: "total request duration by endpoint (transaction boundaries)", + expectedTools: [ + { + name: "datasetAttributes", + arguments: { + dataset: "spans", + }, + }, + ], + expected: { + dataset: "spans", + fields: (value: unknown) => + Array.isArray(value) && + value.includes("transaction") && + !value.includes("sentry.segment.name"), + }, + }, + { + // environment is picked from the common fields + input: + "Show me spans from non production environments in the last 24 hours", + expectedTools: [ + { + name: "datasetAttributes", + arguments: { + dataset: "spans", + }, + }, + ], + expected: { + dataset: "spans", + query: (value: unknown) => + typeof value === "string" && + value.includes("!environment:production") && + !value.includes("sentry.environment"), + }, + }, ]; }, task: async (input) => { @@ -95,7 +133,6 @@ describeEval("search-events-agent-attributes-without-context", { ], }); -// Context not enabled yet so these should not show the correct queries. Asserting them to the wrong queries for now. describeEval("search-events-agent-attributes-with-context", { data: async () => { return [ @@ -138,6 +175,56 @@ describeEval("search-events-agent-attributes-with-context", { !value.includes("http.status_code"), }, }, + { + // sentry.segment.name is picked from span fields due to transaction deprecation and validation endpoint passing + // even though transaction is shown in example queries + input: "total request duration by endpoint (transaction boundaries)", + expectedTools: [ + { + name: "datasetAttributes", + arguments: { + dataset: "spans", + }, + }, + { + name: "validateSearch", + arguments: { + dataset: "spans", + fields: ["sentry.segment.name"], + }, + }, + ], + expected: { + dataset: "spans", + fields: (value: unknown) => + Array.isArray(value) && + !value.includes("transaction") && + value.includes("sentry.segment.name"), + }, + }, + { + // Context marks environment as deprecated in favor of + // sentry.environment + input: + "Show me spans from non production environments in the last 24 hours", + expectedTools: [ + { + name: "datasetAttributes", + arguments: { + dataset: "spans", + }, + }, + ], + expected: { + dataset: "spans", + // sentry.environment:production contains environment:production, so + // look for a bare environment key not preceded by "sentry." + query: (value: unknown) => + typeof value === "string" && + value.includes("!sentry.environment:production") && + !/(? { diff --git a/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-number-with-context.json b/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-number-with-context.json index e53191dcf..b44345c04 100644 --- a/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-number-with-context.json +++ b/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-number-with-context.json @@ -24,16 +24,6 @@ "replacementAttribute": "http.response.status_code" } }, - { - "key": "http.response.status_code", - "name": "http.response.status_code", - "context": { - "isConvention": true, - "brief": "The status code of the HTTP response.", - "isDeprecated": false, - "examples": [404] - } - }, { "key": "custom.count", "name": "custom.count", diff --git a/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-string-with-context.json b/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-string-with-context.json index ed5e763d6..c058e04c5 100644 --- a/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-string-with-context.json +++ b/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-string-with-context.json @@ -234,18 +234,6 @@ "replacementAttribute": "user.geo.region" } }, - { - "key": "user.geo.region", - "name": "user.geo.region", - "context": { - "isConvention": true, - "brief": "The region of the user's location.", - "isDeprecated": false, - "examples": [ - "Canada" - ] - } - }, { "key": "geo.city", "name": "geo.city", From 40a8b51c28c5a61885f2596ad518dd1a101ab5f4 Mon Sep 17 00:00:00 2001 From: nikkikapadia Date: Fri, 9 Oct 2026 12:20:50 -0400 Subject: [PATCH 3/3] cursor fixes --- .../src/tools/support/search-events/utils.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/mcp-core/src/tools/support/search-events/utils.ts b/packages/mcp-core/src/tools/support/search-events/utils.ts index abd7aa324..a8d699d7c 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.ts @@ -970,9 +970,13 @@ function toAttributeRecord( * Swap each deprecated key for its replacement key when available, * otherwise remove the deprecated attribute from the available attributes. */ -function resolveAvailableFields(fields: Record): { +function resolveAvailableFields( + fields: Record, + fieldTypes: Record, +): { available: Map; replaced: AttributeDescriptor[]; + updatedFieldTypes: Record; } { const available = new Map(); const replaced: AttributeDescriptor[] = []; @@ -985,22 +989,22 @@ function resolveAvailableFields(fields: Record): { : undefined; if (attributeDeprecated && !replacementKey) { continue; - } else if (replacementKey) { + } else if (attributeDeprecated && replacementKey) { const replacementAttribute = { ...field, key: replacementKey }; available.set(replacementKey, replacementAttribute); replaced.push(field); - } - - if (!available.has(key)) { + fieldTypes[replacementKey] = fieldTypes[key]; + delete fieldTypes[key]; + } else if (!attributeDeprecated && !available.has(key)) { available.set(key, field); } } - return { available, replaced }; + return { available, replaced, updatedFieldTypes: fieldTypes }; } /** - * Describe a field for the agent, leading with deprecation so it isn't missed + * Describe a field for the agent with examples is provided. */ function describeField(field: AttributeDescriptor): string { const { context } = field; @@ -1091,8 +1095,11 @@ export function createDatasetAttributesTool(options: { ...customAttributes, }; - const { available: availableFields, replaced: replacedFields } = - resolveAvailableFields(allFields); + const { + available: availableFields, + replaced: replacedFields, + updatedFieldTypes, + } = resolveAvailableFields(allFields, fieldTypes); const replacedKeys = new Set(replacedFields.map((field) => field.key)); const fieldCount = availableFields.size; @@ -1100,7 +1107,7 @@ export function createDatasetAttributesTool(options: { // Combine field types from both static config and dynamic API const allFieldTypes: Record = { - ...fieldTypes, + ...updatedFieldTypes, }; const staticNumericFields = NUMERIC_FIELDS[normalizedDataset] || new Set();