Skip to content

Commit dfaa8c8

Browse files
authored
feat(tools): Add find_dropped_events tool for the events-dropped endpoint (#1410)
Adds a `find_dropped_events` tool that exposes Sentry's dedicated **events-dropped** endpoint to agents. It returns ground-truth data-fidelity information — what Sentry received but **dropped** (rate limited, over quota, filtered, invalid, abuse/spike protection, client-discarded via `sample_rate`/`before_send`, cardinality limited) — bucketed over time, plus the **accepted** volume per bucket so a caller can compute the dropped share. Refs DAIN-1863
1 parent 0f68091 commit dfaa8c8

11 files changed

Lines changed: 629 additions & 2 deletions

File tree

‎packages/mcp-core/src/api-client/client.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
DashboardSchema,
5959
DeployListSchema,
6060
DetectorSchema,
61+
DroppedEventsResponseSchema,
6162
ErrorsSearchResponseSchema,
6263
EventAttachmentListSchema,
6364
EventSchema,
@@ -5212,6 +5213,54 @@ export class SentryApiService {
52125213
return EventsStatsResponseSchema.parse(body);
52135214
}
52145215

5216+
async getDroppedEvents(
5217+
{
5218+
organizationSlug,
5219+
interval,
5220+
projectId,
5221+
dataset = "spans",
5222+
statsPeriod,
5223+
start,
5224+
end,
5225+
outcome,
5226+
reason,
5227+
}: {
5228+
organizationSlug: string;
5229+
interval?: string;
5230+
projectId?: string;
5231+
dataset?: EventsDataset;
5232+
statsPeriod?: string;
5233+
start?: string;
5234+
end?: string;
5235+
outcome?: string;
5236+
reason?: string;
5237+
},
5238+
opts?: RequestOptions,
5239+
) {
5240+
const queryParams = new URLSearchParams();
5241+
queryParams.set("dataset", normalizeEventsDataset(dataset));
5242+
if (interval) {
5243+
queryParams.set("interval", interval);
5244+
}
5245+
this.applyTimeParams(queryParams, statsPeriod, start, end);
5246+
if (projectId) {
5247+
queryParams.set("project", projectId);
5248+
}
5249+
if (outcome) {
5250+
queryParams.set("outcome", outcome);
5251+
}
5252+
if (reason) {
5253+
queryParams.set("reason", reason);
5254+
}
5255+
queryParams.set("referrer", SENTRY_MCP_SEARCH_EVENTS_REFERRER);
5256+
5257+
const apiUrl =
5258+
apiPath`/organizations/${organizationSlug}/events-dropped/` +
5259+
`?${queryParams.toString()}`;
5260+
const body = await this.requestJSON(apiUrl, undefined, opts);
5261+
return DroppedEventsResponseSchema.parse(body);
5262+
}
5263+
52155264
// POST https://us.sentry.io/api/0/issues/5485083130/autofix/
52165265
async startAutofix(
52175266
{

‎packages/mcp-core/src/api-client/schema.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2454,3 +2454,30 @@ export const EventsStatsResponseSchema = z
24542454
end: z.number().optional(),
24552455
})
24562456
.passthrough();
2457+
2458+
export const DroppedEventsBucketSchema = z
2459+
.object({
2460+
type: z.string(),
2461+
category: z.string(),
2462+
outcome: z.string(),
2463+
reason: z.string(),
2464+
start: z.number(),
2465+
end: z.number(),
2466+
count: z.number(),
2467+
})
2468+
.passthrough();
2469+
2470+
export const DroppedEventsResponseSchema = z
2471+
.object({
2472+
meta: z
2473+
.object({
2474+
dataset: z.string(),
2475+
start: z.number(),
2476+
end: z.number(),
2477+
interval: z.number(),
2478+
})
2479+
.passthrough(),
2480+
droppedEvents: z.array(DroppedEventsBucketSchema),
2481+
acceptedEvents: z.array(DroppedEventsBucketSchema),
2482+
})
2483+
.passthrough();

‎packages/mcp-core/src/server.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ async function callRegisteredTool(
119119
const DEFAULT_DIRECT_TOOL_NAMES = [
120120
"analyze_issue_with_seer",
121121
"execute_sentry_tool",
122+
"find_dropped_events",
122123
"find_organizations",
123124
"find_projects",
124125
"get_sentry_resource",
@@ -1152,7 +1153,7 @@ describe("buildServer", () => {
11521153

11531154
const result = await callRegisteredTool(server, "search_sentry_tools", {
11541155
query: "event stacktrace",
1155-
limit: 5,
1156+
limit: 8,
11561157
});
11571158
const payload = getStructuredContent<{
11581159
results: Array<{ name: string }>;

‎packages/mcp-core/src/skillDefinitions.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, cron monitors, uptime monitors, metric monitors, profiles, documentation, and project metadata",
66
"defaultEnabled": true,
77
"order": 1,
8-
"toolCount": 46,
8+
"toolCount": 47,
99
"tools": [
1010
{
1111
"name": "find_alert_rules",
@@ -17,6 +17,11 @@
1717
"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<examples>\nfind_dashboards(organizationSlug='my-organization')\nfind_dashboards(organizationSlug='my-organization', titleQuery='errors')\n</examples>\n\n<hints>\n- Dashboard IDs are organization-scoped.\n- Use `get_dashboard_details` after finding the correct dashboard ID.\n</hints>",
1818
"requiredScopes": ["org:read"]
1919
},
20+
{
21+
"name": "find_dropped_events",
22+
"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<examples>\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\nfind_dropped_events(organizationSlug='my-org', dataset='errors', outcome='rate_limited')\n</examples>\n\n<hints>\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- Pass `outcome` and/or `reason` to scope the dropped side to one classification; the accepted volume is always returned in full.\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n</hints>",
23+
"requiredScopes": ["event:read"]
24+
},
2025
{
2126
"name": "find_metric_monitors",
2227
"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')",

‎packages/mcp-core/src/toolDefinitions.json‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,216 @@
22212221
"skills": ["inspect"],
22222222
"surface": "catalog"
22232223
},
2224+
{
2225+
"name": "find_dropped_events",
2226+
"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<examples>\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\nfind_dropped_events(organizationSlug='my-org', dataset='errors', outcome='rate_limited')\n</examples>\n\n<hints>\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- Pass `outcome` and/or `reason` to scope the dropped side to one classification; the accepted volume is always returned in full.\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\n</hints>",
2227+
"inputSchema": {
2228+
"type": "object",
2229+
"properties": {
2230+
"organizationSlug": {
2231+
"type": "string",
2232+
"description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
2233+
},
2234+
"regionUrl": {
2235+
"default": null,
2236+
"anyOf": [
2237+
{
2238+
"type": "string",
2239+
"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."
2240+
},
2241+
{
2242+
"type": "null"
2243+
}
2244+
]
2245+
},
2246+
"dataset": {
2247+
"default": "spans",
2248+
"type": "string",
2249+
"enum": ["spans", "logs", "metrics", "errors"],
2250+
"description": "Which data type to report drops for."
2251+
},
2252+
"projectSlug": {
2253+
"default": null,
2254+
"anyOf": [
2255+
{
2256+
"type": "string",
2257+
"description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
2258+
},
2259+
{
2260+
"type": "null"
2261+
}
2262+
]
2263+
},
2264+
"statsPeriod": {
2265+
"default": null,
2266+
"anyOf": [
2267+
{
2268+
"type": "string",
2269+
"description": "Relative time range, e.g. '24h', '7d', '30d'. Mutually exclusive with start/end."
2270+
},
2271+
{
2272+
"type": "null"
2273+
}
2274+
]
2275+
},
2276+
"start": {
2277+
"default": null,
2278+
"anyOf": [
2279+
{
2280+
"type": "string",
2281+
"description": "Absolute start (ISO 8601). Must be paired with end."
2282+
},
2283+
{
2284+
"type": "null"
2285+
}
2286+
]
2287+
},
2288+
"end": {
2289+
"default": null,
2290+
"anyOf": [
2291+
{
2292+
"type": "string",
2293+
"description": "Absolute end (ISO 8601). Must be paired with start."
2294+
},
2295+
{
2296+
"type": "null"
2297+
}
2298+
]
2299+
},
2300+
"interval": {
2301+
"default": null,
2302+
"anyOf": [
2303+
{
2304+
"type": "string",
2305+
"description": "Bucket size, e.g. '1h', '1d'. Omit to let Sentry pick for the range."
2306+
},
2307+
{
2308+
"type": "null"
2309+
}
2310+
]
2311+
},
2312+
"outcome": {
2313+
"default": null,
2314+
"anyOf": [
2315+
{
2316+
"type": "string",
2317+
"enum": [
2318+
"rate_limited",
2319+
"filtered",
2320+
"invalid",
2321+
"abuse",
2322+
"client_discard",
2323+
"cardinality_limited"
2324+
],
2325+
"description": "Scope the dropped side to one top-level drop classification. Accepted volume is still returned in full."
2326+
},
2327+
{
2328+
"type": "null"
2329+
}
2330+
]
2331+
},
2332+
"reason": {
2333+
"default": null,
2334+
"anyOf": [
2335+
{
2336+
"type": "string",
2337+
"description": "Scope the dropped side to one reason (sub-classification within an outcome, e.g. 'spike_protection'). Combine with `outcome`."
2338+
},
2339+
{
2340+
"type": "null"
2341+
}
2342+
]
2343+
}
2344+
},
2345+
"required": ["organizationSlug"]
2346+
},
2347+
"outputSchema": {
2348+
"type": "object",
2349+
"properties": {
2350+
"dataset": {
2351+
"type": "string"
2352+
},
2353+
"interval": {
2354+
"type": "number"
2355+
},
2356+
"droppedEvents": {
2357+
"type": "array",
2358+
"items": {
2359+
"type": "object",
2360+
"properties": {
2361+
"outcome": {
2362+
"type": "string"
2363+
},
2364+
"reason": {
2365+
"type": "string"
2366+
},
2367+
"category": {
2368+
"type": "string"
2369+
},
2370+
"start": {
2371+
"type": "number"
2372+
},
2373+
"end": {
2374+
"type": "number"
2375+
},
2376+
"count": {
2377+
"type": "number"
2378+
}
2379+
},
2380+
"required": [
2381+
"outcome",
2382+
"reason",
2383+
"category",
2384+
"start",
2385+
"end",
2386+
"count"
2387+
],
2388+
"additionalProperties": false
2389+
}
2390+
},
2391+
"acceptedEvents": {
2392+
"type": "array",
2393+
"items": {
2394+
"type": "object",
2395+
"properties": {
2396+
"outcome": {
2397+
"type": "string"
2398+
},
2399+
"reason": {
2400+
"type": "string"
2401+
},
2402+
"category": {
2403+
"type": "string"
2404+
},
2405+
"start": {
2406+
"type": "number"
2407+
},
2408+
"end": {
2409+
"type": "number"
2410+
},
2411+
"count": {
2412+
"type": "number"
2413+
}
2414+
},
2415+
"required": [
2416+
"outcome",
2417+
"reason",
2418+
"category",
2419+
"start",
2420+
"end",
2421+
"count"
2422+
],
2423+
"additionalProperties": false
2424+
}
2425+
}
2426+
},
2427+
"required": ["dataset", "interval", "droppedEvents", "acceptedEvents"],
2428+
"additionalProperties": false
2429+
},
2430+
"requiredScopes": ["event:read"],
2431+
"skills": ["inspect"],
2432+
"surface": "direct"
2433+
},
22242434
{
22252435
"name": "find_dsns",
22262436
"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<hints>\n- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.\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</hints>",

0 commit comments

Comments
 (0)