From 8120910b8771563e217f695c276223f2ddd8524d Mon Sep 17 00:00:00 2001 From: Saraj Manes Date: Thu, 1 Oct 2026 11:58:01 -0400 Subject: [PATCH 1/4] feat(tools): Add find_dropped_events tool for the events-dropped endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes Sentry's dedicated events-dropped endpoint to agents as a new find_dropped_events tool: ground-truth data-fidelity information (what Sentry received but dropped, and why) bucketed over time, alongside accepted volume so a caller can compute the dropped share. Surfaced directly (same tier as search_events) since an agent debugging a flat/spiky/low chart should be able to reach it without discovery. - api-client: getDroppedEvents() + DroppedEvents response schemas - tool: find_dropped_events (datasets: spans, logs, metrics) - registered in the catalog + direct surface; regenerated definitions Backed by an agent eval: naive agents given a user-phrased question reliably discovered and called this tool, and attributed flat/spiky charts to the right drop reason — where the same agents without it concluded "nothing is broken." Refs DAIN-1863 --- packages/mcp-core/src/api-client/client.ts | 45 +++++ packages/mcp-core/src/api-client/schema.ts | 37 ++++ packages/mcp-core/src/skillDefinitions.json | 7 +- packages/mcp-core/src/toolDefinitions.json | 178 ++++++++++++++++++ .../tools/catalog/find-dropped-events.test.ts | 107 +++++++++++ .../src/tools/catalog/find-dropped-events.ts | 161 ++++++++++++++++ packages/mcp-core/src/tools/catalog/index.ts | 2 + packages/mcp-core/src/tools/surfaces.ts | 1 + .../agents/sentry-mcp.md | 1 + plugins/sentry-mcp/agents/sentry-mcp.md | 1 + 10 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 packages/mcp-core/src/tools/catalog/find-dropped-events.test.ts create mode 100644 packages/mcp-core/src/tools/catalog/find-dropped-events.ts diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 115a3687c..71670c717 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -57,6 +57,7 @@ import { DashboardSchema, DeployListSchema, DetectorSchema, + DroppedEventsResponseSchema, ErrorsSearchResponseSchema, EventAttachmentListSchema, EventSchema, @@ -5207,6 +5208,50 @@ export class SentryApiService { return EventsStatsResponseSchema.parse(body); } + /** + * Fetch dropped (and accepted) event volume from the events-dropped endpoint, + * bucketed over time. This is ground-truth data-fidelity information: what + * Sentry received but discarded, and why. Unlike events-stats, this runs no + * chart query and takes no search filter — it reads Outcomes directly. + */ + async getDroppedEvents( + { + organizationSlug, + interval, + projectId, + dataset = "spans", + statsPeriod, + start, + end, + }: { + organizationSlug: string; + interval?: string; + projectId?: string; + dataset?: EventsDataset; + statsPeriod?: string; + start?: string; + end?: string; + }, + opts?: RequestOptions, + ) { + const queryParams = new URLSearchParams(); + queryParams.set("dataset", normalizeEventsDataset(dataset)); + if (interval) { + queryParams.set("interval", interval); + } + this.applyTimeParams(queryParams, statsPeriod, start, end); + if (projectId) { + queryParams.set("project", projectId); + } + queryParams.set("referrer", SENTRY_MCP_SEARCH_EVENTS_REFERRER); + + const apiUrl = + apiPath`/organizations/${organizationSlug}/events-dropped/` + + `?${queryParams.toString()}`; + const body = await this.requestJSON(apiUrl, undefined, opts); + return DroppedEventsResponseSchema.parse(body); + } + // POST https://us.sentry.io/api/0/issues/5485083130/autofix/ async startAutofix( { diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index cccc58224..c54c6ea87 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -2454,3 +2454,40 @@ export const EventsStatsResponseSchema = z end: z.number().optional(), }) .passthrough(); + +/** + * One time bucket from the events-dropped endpoint. `outcome` is the drop kind + * (rate_limited, filtered, invalid, abuse, client_discard, cardinality_limited) + * and `reason` the sub-cause; both are "accepted" on accepted buckets. + */ +export const DroppedEventsBucketSchema = z + .object({ + type: z.string(), + category: z.string(), + outcome: z.string(), + reason: z.string(), + start: z.number(), + end: z.number(), + count: z.number(), + }) + .passthrough(); + +/** + * Response from the events-dropped endpoint: dropped and accepted event volume + * bucketed over the requested interval. `acceptedEvents` is the share + * denominator for the drops in the same window. + */ +export const DroppedEventsResponseSchema = z + .object({ + meta: z + .object({ + dataset: z.string(), + start: z.number(), + end: z.number(), + interval: z.number(), + }) + .passthrough(), + droppedEvents: z.array(DroppedEventsBucketSchema), + acceptedEvents: z.array(DroppedEventsBucketSchema), + }) + .passthrough(); diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 44b41ac87..58ce5cadd 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -5,7 +5,7 @@ "description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, cron monitors, uptime monitors, metric monitors, profiles, documentation, and project metadata", "defaultEnabled": true, "order": 1, - "toolCount": 46, + "toolCount": 47, "tools": [ { "name": "find_alert_rules", @@ -17,6 +17,11 @@ "description": "Find Sentry dashboards in an organization.\n\nUse this tool when you need to:\n- List dashboards in an organization\n- Find a dashboard ID before calling get_dashboard_details\n- Search dashboards by title\n\n\nfind_dashboards(organizationSlug='my-organization')\nfind_dashboards(organizationSlug='my-organization', titleQuery='errors')\n\n\n\n- Dashboard IDs are organization-scoped.\n- Use `get_dashboard_details` after finding the correct dashboard ID.\n", "requiredScopes": ["org:read"] }, + { + "name": "find_dropped_events", + "description": "Find events Sentry received but dropped — ground-truth data-fidelity information.\n\nEvents can be dropped before they reach a chart (rate limited, over quota, filtered,\ninvalid, abuse/spike protection, client-discarded via sample_rate or before_send,\ncardinality limited). A normal timeseries only shows what was accepted, so a flat,\nspiky, or unexpectedly low chart can be caused entirely by drops that the chart does\nnot reveal.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Check whether data was dropped before trusting a query result or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n", + "requiredScopes": ["event:read"] + }, { "name": "find_metric_monitors", "description": "Find Sentry Metric Monitors that evaluate errors, performance, logs, metrics or crash rates.\nUse this tool to find a monitor ID before inspecting its query and detection conditions with get_metric_monitor_details.\nResults contain native monitor IDs, separate from legacy metric alert IDs. Alerts connected through workflowIds control notifications; inspect them with get_alert_rule(kind='issue').\nOmit projectSlug to search all accessible projects. Pass nextCursor as cursor with the same filters to retrieve more results.\nfind_metric_monitors(organizationSlug='my-org', projectSlug='backend', query='latency')", diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 9fa64538c..331cafd88 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -2221,6 +2221,184 @@ "skills": ["inspect"], "surface": "catalog" }, + { + "name": "find_dropped_events", + "description": "Find events Sentry received but dropped — ground-truth data-fidelity information.\n\nEvents can be dropped before they reach a chart (rate limited, over quota, filtered,\ninvalid, abuse/spike protection, client-discarded via sample_rate or before_send,\ncardinality limited). A normal timeseries only shows what was accepted, so a flat,\nspiky, or unexpectedly low chart can be caused entirely by drops that the chart does\nnot reveal.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Check whether data was dropped before trusting a query result or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "dataset": { + "default": "spans", + "type": "string", + "enum": ["spans", "logs", "metrics"], + "description": "Which data type to report drops for." + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "statsPeriod": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Relative time range, e.g. '24h', '7d', '30d'. Mutually exclusive with start/end." + }, + { + "type": "null" + } + ] + }, + "start": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Absolute start (ISO 8601). Must be paired with end." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Absolute end (ISO 8601). Must be paired with start." + }, + { + "type": "null" + } + ] + }, + "interval": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Bucket size, e.g. '1h', '1d'. Omit to let Sentry pick for the range." + }, + { + "type": "null" + } + ] + } + }, + "required": ["organizationSlug"] + }, + "outputSchema": { + "type": "object", + "properties": { + "dataset": { + "type": "string" + }, + "interval": { + "type": "number" + }, + "droppedEvents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "outcome": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "category": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "count": { + "type": "number" + } + }, + "required": [ + "outcome", + "reason", + "category", + "start", + "end", + "count" + ], + "additionalProperties": false + } + }, + "acceptedEvents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "outcome": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "category": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "count": { + "type": "number" + } + }, + "required": [ + "outcome", + "reason", + "category", + "start", + "end", + "count" + ], + "additionalProperties": false + } + } + }, + "required": ["dataset", "interval", "droppedEvents", "acceptedEvents"], + "additionalProperties": false + }, + "requiredScopes": ["event:read"], + "skills": ["inspect"], + "surface": "direct" + }, { "name": "find_dsns", "description": "List all Sentry DSNs for a specific project.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n", diff --git a/packages/mcp-core/src/tools/catalog/find-dropped-events.test.ts b/packages/mcp-core/src/tools/catalog/find-dropped-events.test.ts new file mode 100644 index 000000000..d48efe1f0 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/find-dropped-events.test.ts @@ -0,0 +1,107 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; +import { describe, expect, it } from "vitest"; +import { + assertStructuredOnlyResult, + getStructuredContent, +} from "../../test-utils/structured-content.js"; +import findDroppedEvents, { + findDroppedEventsOutputSchema, +} from "./find-dropped-events.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +const DROPPED_EVENTS_RESPONSE = { + meta: { + dataset: "spans", + start: 1_700_000_000_000, + end: 1_700_003_600_000, + interval: 3_600_000, + }, + droppedEvents: [ + { + type: "system", + category: "span", + outcome: "rate_limited", + reason: "key_quota", + start: 1_700_000_000_000, + end: 1_700_003_600_000, + count: 400, + }, + ], + acceptedEvents: [ + { + type: "system", + category: "span", + outcome: "accepted", + reason: "accepted", + start: 1_700_000_000_000, + end: 1_700_003_600_000, + count: 1000, + }, + ], +}; + +describe("find_dropped_events", () => { + it("returns dropped and accepted buckets for an org", async () => { + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/events-dropped/", + () => HttpResponse.json(DROPPED_EVENTS_RESPONSE), + ), + ); + + const result = await findDroppedEvents.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + dataset: "spans", + projectSlug: null, + statsPeriod: "24h", + start: null, + end: null, + interval: null, + }, + context, + ); + + assertStructuredOnlyResult(result); + const structuredContent = getStructuredContent(result); + expect(findDroppedEvents.outputSchema).toBe(findDroppedEventsOutputSchema); + expect(findDroppedEventsOutputSchema.parse(structuredContent)).toEqual( + structuredContent, + ); + expect(structuredContent).toMatchInlineSnapshot(` + { + "acceptedEvents": [ + { + "category": "span", + "count": 1000, + "end": 1700003600000, + "outcome": "accepted", + "reason": "accepted", + "start": 1700000000000, + }, + ], + "dataset": "spans", + "droppedEvents": [ + { + "category": "span", + "count": 400, + "end": 1700003600000, + "outcome": "rate_limited", + "reason": "key_quota", + "start": 1700000000000, + }, + ], + "interval": 3600000, + } + `); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/find-dropped-events.ts b/packages/mcp-core/src/tools/catalog/find-dropped-events.ts new file mode 100644 index 000000000..4df203c8f --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/find-dropped-events.ts @@ -0,0 +1,161 @@ +import { z } from "zod"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { structuredResult } from "../../internal/tool-helpers/results"; +import { + ParamOrganizationSlug, + ParamProjectSlug, + ParamRegionUrl, +} from "../../schema"; +import { setTargetTagsAndAttributes } from "../../telem/scope"; +import type { ServerContext } from "../../types"; + +// events-dropped only supports the datasets in its DATASET_TO_CATEGORY map. +const DROPPED_EVENTS_DATASETS = ["spans", "logs", "metrics"] as const; + +const droppedBucketSchema = z.object({ + outcome: z.string(), + reason: z.string(), + category: z.string(), + start: z.number(), + end: z.number(), + count: z.number(), +}); + +export const findDroppedEventsOutputSchema = z.object({ + dataset: z.string(), + interval: z.number(), + droppedEvents: z.array(droppedBucketSchema), + acceptedEvents: z.array(droppedBucketSchema), +}); + +export default defineTool({ + name: "find_dropped_events", + skills: ["inspect"], + requiredScopes: ["event:read"], + description: [ + "Find events Sentry received but dropped — ground-truth data-fidelity information.", + "", + "Events can be dropped before they reach a chart (rate limited, over quota, filtered,", + "invalid, abuse/spike protection, client-discarded via sample_rate or before_send,", + "cardinality limited). A normal timeseries only shows what was accepted, so a flat,", + "spiky, or unexpectedly low chart can be caused entirely by drops that the chart does", + "not reveal.", + "", + "Use this tool when you need to:", + "- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending", + "- Check whether data was dropped before trusting a query result or dashboard", + "- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)", + "- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)", + "", + "Returns dropped event volume bucketed over time, with the drop `outcome` and `reason`", + "for each bucket, plus the accepted volume per bucket so you can compute the dropped share.", + "", + "", + "find_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')", + "find_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')", + "", + "", + "", + "- This is independent of any search query — it reports drops for the whole project/time range.", + "- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).", + "- An empty `droppedEvents` list means no drops in the window — the data can be trusted.", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + dataset: z + .enum(DROPPED_EVENTS_DATASETS) + .describe("Which data type to report drops for.") + .default("spans"), + projectSlug: ParamProjectSlug.nullable().default(null), + statsPeriod: z + .string() + .trim() + .describe( + "Relative time range, e.g. '24h', '7d', '30d'. Mutually exclusive with start/end.", + ) + .nullable() + .default(null), + start: z + .string() + .trim() + .describe("Absolute start (ISO 8601). Must be paired with end.") + .nullable() + .default(null), + end: z + .string() + .trim() + .describe("Absolute end (ISO 8601). Must be paired with start.") + .nullable() + .default(null), + interval: z + .string() + .trim() + .describe( + "Bucket size, e.g. '1h', '1d'. Omit to let Sentry pick for the range.", + ) + .nullable() + .default(null), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + outputSchema: findDroppedEventsOutputSchema, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + const organizationSlug = params.organizationSlug; + + setTargetTagsAndAttributes({ + organizationSlug, + projectSlug: params.projectSlug ?? undefined, + }); + + let projectId: string | undefined; + if (params.projectSlug) { + const project = await apiService.getProject({ + organizationSlug, + projectSlugOrId: params.projectSlug, + }); + projectId = String(project.id); + } + + const response = await apiService.getDroppedEvents({ + organizationSlug, + dataset: params.dataset, + projectId, + interval: params.interval ?? undefined, + statsPeriod: params.statsPeriod ?? undefined, + start: params.start ?? undefined, + end: params.end ?? undefined, + }); + + const toBucket = (bucket: { + outcome: string; + reason: string; + category: string; + start: number; + end: number; + count: number; + }) => ({ + outcome: bucket.outcome, + reason: bucket.reason, + category: bucket.category, + start: bucket.start, + end: bucket.end, + count: bucket.count, + }); + + return structuredResult({ + dataset: response.meta.dataset, + interval: response.meta.interval, + droppedEvents: response.droppedEvents.map(toBucket), + acceptedEvents: response.acceptedEvents.map(toBucket), + }); + }, +}); diff --git a/packages/mcp-core/src/tools/catalog/index.ts b/packages/mcp-core/src/tools/catalog/index.ts index dbe714170..be6697cb3 100644 --- a/packages/mcp-core/src/tools/catalog/index.ts +++ b/packages/mcp-core/src/tools/catalog/index.ts @@ -45,6 +45,7 @@ import searchTraces from "./search-traces"; import searchMetrics from "./search-metrics"; import searchProfiles from "./search-profiles"; import searchReplays from "./search-replays"; +import findDroppedEvents from "./find-dropped-events"; import createTeam from "./create-team"; import createProject from "./create-project"; import updateProject from "./update-project"; @@ -161,6 +162,7 @@ const catalogTools = { get_doc: getDoc, search_issues: searchIssues, search_issue_events: searchIssueEvents, + find_dropped_events: findDroppedEvents, get_profile: getProfile, get_profile_details: getProfileDetails, get_sentry_mcp_info: getSentryMcpInfo, diff --git a/packages/mcp-core/src/tools/surfaces.ts b/packages/mcp-core/src/tools/surfaces.ts index 571459f2f..dd78a7cb7 100644 --- a/packages/mcp-core/src/tools/surfaces.ts +++ b/packages/mcp-core/src/tools/surfaces.ts @@ -24,6 +24,7 @@ export const TOP_LEVEL_TOOL_NAMES = [ "search_metrics", "search_profiles", "search_replays", + "find_dropped_events", "analyze_issue_with_seer", "search_issues", "get_sentry_resource", diff --git a/plugins/sentry-mcp-experimental/agents/sentry-mcp.md b/plugins/sentry-mcp-experimental/agents/sentry-mcp.md index 37705a4f0..eebfb87b7 100644 --- a/plugins/sentry-mcp-experimental/agents/sentry-mcp.md +++ b/plugins/sentry-mcp-experimental/agents/sentry-mcp.md @@ -12,6 +12,7 @@ mcpServers: allowedTools: - analyze_issue_with_seer - execute_sentry_tool + - find_dropped_events - find_organizations - find_projects - get_sentry_resource diff --git a/plugins/sentry-mcp/agents/sentry-mcp.md b/plugins/sentry-mcp/agents/sentry-mcp.md index 37705a4f0..eebfb87b7 100644 --- a/plugins/sentry-mcp/agents/sentry-mcp.md +++ b/plugins/sentry-mcp/agents/sentry-mcp.md @@ -12,6 +12,7 @@ mcpServers: allowedTools: - analyze_issue_with_seer - execute_sentry_tool + - find_dropped_events - find_organizations - find_projects - get_sentry_resource From fa91913c8bed8cca9dadda31b2b8940932d30f20 Mon Sep 17 00:00:00 2001 From: Saraj Manes Date: Mon, 5 Oct 2026 12:53:45 -0400 Subject: [PATCH 2/4] ref(tools): Drop narrative comments from dropped-events code Remove doc comments that restated project framing / dev decisions rather than explaining non-obvious code. Matches the surrounding file convention, where sibling schemas, client methods, and tools carry no such comments. The caller-facing tool description and param descriptions are unchanged. Refs DAIN-1863 --- packages/mcp-core/src/api-client/client.ts | 6 ------ packages/mcp-core/src/api-client/schema.ts | 10 ---------- .../mcp-core/src/tools/catalog/find-dropped-events.ts | 1 - 3 files changed, 17 deletions(-) diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 71670c717..bdf933a22 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -5208,12 +5208,6 @@ export class SentryApiService { return EventsStatsResponseSchema.parse(body); } - /** - * Fetch dropped (and accepted) event volume from the events-dropped endpoint, - * bucketed over time. This is ground-truth data-fidelity information: what - * Sentry received but discarded, and why. Unlike events-stats, this runs no - * chart query and takes no search filter — it reads Outcomes directly. - */ async getDroppedEvents( { organizationSlug, diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index c54c6ea87..66bc33a20 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -2455,11 +2455,6 @@ export const EventsStatsResponseSchema = z }) .passthrough(); -/** - * One time bucket from the events-dropped endpoint. `outcome` is the drop kind - * (rate_limited, filtered, invalid, abuse, client_discard, cardinality_limited) - * and `reason` the sub-cause; both are "accepted" on accepted buckets. - */ export const DroppedEventsBucketSchema = z .object({ type: z.string(), @@ -2472,11 +2467,6 @@ export const DroppedEventsBucketSchema = z }) .passthrough(); -/** - * Response from the events-dropped endpoint: dropped and accepted event volume - * bucketed over the requested interval. `acceptedEvents` is the share - * denominator for the drops in the same window. - */ export const DroppedEventsResponseSchema = z .object({ meta: z diff --git a/packages/mcp-core/src/tools/catalog/find-dropped-events.ts b/packages/mcp-core/src/tools/catalog/find-dropped-events.ts index 4df203c8f..1b1b89527 100644 --- a/packages/mcp-core/src/tools/catalog/find-dropped-events.ts +++ b/packages/mcp-core/src/tools/catalog/find-dropped-events.ts @@ -10,7 +10,6 @@ import { import { setTargetTagsAndAttributes } from "../../telem/scope"; import type { ServerContext } from "../../types"; -// events-dropped only supports the datasets in its DATASET_TO_CATEGORY map. const DROPPED_EVENTS_DATASETS = ["spans", "logs", "metrics"] as const; const droppedBucketSchema = z.object({ From 81bee01b688554b3f85bd742787eef9aa2b489b6 Mon Sep 17 00:00:00 2001 From: Saraj Manes Date: Mon, 5 Oct 2026 14:29:06 -0400 Subject: [PATCH 3/4] ref(tools): Broaden find_dropped_events description beyond charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three targeted edits to the tool description, keeping the tested structure: - Lead with what the tool captures (dropped before storage), not "received" — SDK-side drops (sample_rate, before_send) never reach Sentry. - Frame charts as one example of an accepted-only view, not the purpose; the tool's role is ground-truth data fidelity: is the data in Sentry or dropped. - Reframe the trust bullet around confirming data is actually in Sentry before relying on a query, aggregate, or dashboard. Regenerated tool/skill definitions. Refs DAIN-1863 --- packages/mcp-core/src/skillDefinitions.json | 2 +- packages/mcp-core/src/toolDefinitions.json | 2 +- .../src/tools/catalog/find-dropped-events.ts | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 58ce5cadd..6ef639c96 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -19,7 +19,7 @@ }, { "name": "find_dropped_events", - "description": "Find events Sentry received but dropped — ground-truth data-fidelity information.\n\nEvents can be dropped before they reach a chart (rate limited, over quota, filtered,\ninvalid, abuse/spike protection, client-discarded via sample_rate or before_send,\ncardinality limited). A normal timeseries only shows what was accepted, so a flat,\nspiky, or unexpectedly low chart can be caused entirely by drops that the chart does\nnot reveal.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Check whether data was dropped before trusting a query result or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n", + "description": "Find events dropped before they were stored in Sentry — ground-truth data-fidelity information about what was and wasn't captured.\n\nEvents can be dropped client-side in the SDK (sample_rate, before_send) or\nserver-side at ingest (rate limited, over quota, filtered, invalid, abuse/spike\nprotection, cardinality limited). Accepted-only views (searches, aggregates,\ncharts) can't show this, so the data may be incomplete in ways they don't reveal\n— for example, a flat or spiky chart caused entirely by drops.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Confirm the data you need is actually in Sentry (not dropped) before trusting a query, aggregate, or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n", "requiredScopes": ["event:read"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 331cafd88..0a8fdbaf4 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -2223,7 +2223,7 @@ }, { "name": "find_dropped_events", - "description": "Find events Sentry received but dropped — ground-truth data-fidelity information.\n\nEvents can be dropped before they reach a chart (rate limited, over quota, filtered,\ninvalid, abuse/spike protection, client-discarded via sample_rate or before_send,\ncardinality limited). A normal timeseries only shows what was accepted, so a flat,\nspiky, or unexpectedly low chart can be caused entirely by drops that the chart does\nnot reveal.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Check whether data was dropped before trusting a query result or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n", + "description": "Find events dropped before they were stored in Sentry — ground-truth data-fidelity information about what was and wasn't captured.\n\nEvents can be dropped client-side in the SDK (sample_rate, before_send) or\nserver-side at ingest (rate limited, over quota, filtered, invalid, abuse/spike\nprotection, cardinality limited). Accepted-only views (searches, aggregates,\ncharts) can't show this, so the data may be incomplete in ways they don't reveal\n— for example, a flat or spiky chart caused entirely by drops.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Confirm the data you need is actually in Sentry (not dropped) before trusting a query, aggregate, or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n", "inputSchema": { "type": "object", "properties": { diff --git a/packages/mcp-core/src/tools/catalog/find-dropped-events.ts b/packages/mcp-core/src/tools/catalog/find-dropped-events.ts index 1b1b89527..e4eaa31bc 100644 --- a/packages/mcp-core/src/tools/catalog/find-dropped-events.ts +++ b/packages/mcp-core/src/tools/catalog/find-dropped-events.ts @@ -33,17 +33,17 @@ export default defineTool({ skills: ["inspect"], requiredScopes: ["event:read"], description: [ - "Find events Sentry received but dropped — ground-truth data-fidelity information.", + "Find events dropped before they were stored in Sentry — ground-truth data-fidelity information about what was and wasn't captured.", "", - "Events can be dropped before they reach a chart (rate limited, over quota, filtered,", - "invalid, abuse/spike protection, client-discarded via sample_rate or before_send,", - "cardinality limited). A normal timeseries only shows what was accepted, so a flat,", - "spiky, or unexpectedly low chart can be caused entirely by drops that the chart does", - "not reveal.", + "Events can be dropped client-side in the SDK (sample_rate, before_send) or", + "server-side at ingest (rate limited, over quota, filtered, invalid, abuse/spike", + "protection, cardinality limited). Accepted-only views (searches, aggregates,", + "charts) can't show this, so the data may be incomplete in ways they don't reveal", + "— for example, a flat or spiky chart caused entirely by drops.", "", "Use this tool when you need to:", "- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending", - "- Check whether data was dropped before trusting a query result or dashboard", + "- Confirm the data you need is actually in Sentry (not dropped) before trusting a query, aggregate, or dashboard", "- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)", "- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)", "", From 143768d003d4146ddc3f0930929ad5084def8854 Mon Sep 17 00:00:00 2001 From: Saraj Manes Date: Mon, 5 Oct 2026 15:22:02 -0400 Subject: [PATCH 4/4] test(server): Account for find_dropped_events in the direct surface Adding find_dropped_events to TOP_LEVEL_TOOL_NAMES changes the exact direct tool set these guardrail tests assert, so add it to DEFAULT_DIRECT_TOOL_NAMES. The catalog-search test queried 'event stacktrace' at limit 5; find_dropped_events now ranks there (its name contains 'events'), so raise the limit to 8 to keep asserting that catalog-only tools remain findable via search. Refs DAIN-1863 --- packages/mcp-core/src/server.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mcp-core/src/server.test.ts b/packages/mcp-core/src/server.test.ts index 57c0cb4de..bce1ba369 100644 --- a/packages/mcp-core/src/server.test.ts +++ b/packages/mcp-core/src/server.test.ts @@ -119,6 +119,7 @@ async function callRegisteredTool( const DEFAULT_DIRECT_TOOL_NAMES = [ "analyze_issue_with_seer", "execute_sentry_tool", + "find_dropped_events", "find_organizations", "find_projects", "get_sentry_resource", @@ -1152,7 +1153,7 @@ describe("buildServer", () => { const result = await callRegisteredTool(server, "search_sentry_tools", { query: "event stacktrace", - limit: 5, + limit: 8, }); const payload = getStructuredContent<{ results: Array<{ name: string }>;