diff --git a/docs/contributing/search-events-api-patterns.md b/docs/contributing/search-events-api-patterns.md index b6299c234..e64f585bd 100644 --- a/docs/contributing/search-events-api-patterns.md +++ b/docs/contributing/search-events-api-patterns.md @@ -23,6 +23,7 @@ Sentry uses two different query-building patterns depending on the dataset, even ### API Endpoint All queries use the same base endpoint: + ``` /api/0/organizations/{organizationSlug}/events/ ``` @@ -30,6 +31,7 @@ All queries use the same base endpoint: ### Dataset Mapping The tool forwards the dataset name directly to the API: + - User specifies `errors` → API uses `errors` - User specifies `spans` → API uses `spans` - User specifies `logs` → API uses `logs` @@ -42,12 +44,14 @@ The tool forwards the dataset name directly to the API: Returns raw event data with full details. This is the default mode when no aggregate functions are used. **Key Characteristics:** + - Returns actual event occurrences - Includes default fields plus any user-requested fields - Sorted by timestamp (newest first) by default - Limited to a specific number of results (default: 10, max: 100) **Example API URL:** + ``` https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=id&field=span.op&field=span.description&field=span.duration&field=transaction&field=timestamp&field=ai.model.id&field=ai.model.provider&field=project&field=trace&per_page=50&query=&sort=-timestamp&statsPeriod=24h ``` @@ -64,6 +68,7 @@ https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=id&f Returns grouped and aggregated data, similar to SQL GROUP BY queries. **Key Characteristics:** + - Activated when ANY field contains a function (e.g., `count()`, `avg()`) - Fields should ONLY include aggregate functions and groupBy fields - Do NOT include default fields (id, timestamp, etc.) @@ -72,11 +77,13 @@ Returns grouped and aggregated data, similar to SQL GROUP BY queries. **Example API URLs:** Single groupBy field: + ``` https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=ai.model.id&field=count()&per_page=50&query=&sort=-count()&statsPeriod=24h ``` Multiple groupBy fields: + ``` https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=ai.model.id&field=ai.model.provider&field=sum(span.duration)&per_page=50&query=&sort=-sum(span.duration)&statsPeriod=24h ``` @@ -85,37 +92,40 @@ https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=ai.m ### Common Parameters -| Parameter | Description | Example | -|-----------|-------------|---------| -| `dataset` | Which dataset to query | `spans`, `errors`, `logs`, `metrics` | -| `field` | Fields to return (repeated for each field) | `field=span.op&field=count()` | -| `query` | Sentry query syntax filter | `has:db.statement AND span.duration:>1000` | -| `sort` | Sort order (prefix with `-` for descending) | `-timestamp`, `-count()` | -| `per_page` | Results per page | `50` | -| `statsPeriod` | Relative time window filter | `1h`, `24h`, `7d`, `14d`, `30d` | -| `start` | Absolute start time (ISO 8601) | `2025-06-19T07:00:00` | -| `end` | Absolute end time (ISO 8601) | `2025-06-20T06:59:59` | -| `project` | Project ID (numeric, not slug) | `4509062593708032` | - +| Parameter | Description | Example | +| ------------- | ------------------------------------------- | ------------------------------------------ | +| `dataset` | Which dataset to query | `spans`, `errors`, `logs`, `metrics` | +| `field` | Fields to return (repeated for each field) | `field=span.op&field=count()` | +| `query` | Sentry query syntax filter | `has:db.statement AND span.duration:>1000` | +| `sort` | Sort order (prefix with `-` for descending) | `-timestamp`, `-count()` | +| `per_page` | Results per page | `50` | +| `statsPeriod` | Relative time window filter | `1h`, `24h`, `7d`, `14d`, `30d` | +| `start` | Absolute start time (ISO 8601) | `2025-06-19T07:00:00` | +| `end` | Absolute end time (ISO 8601) | `2025-06-20T06:59:59` | +| `project` | Project ID (numeric, not slug) | `4509062593708032` | ### Dataset-Specific Considerations #### Spans Dataset + - Supports timestamp filters in query (e.g., `timestamp:-1h`) - Rich performance metrics available - Common aggregate functions: `count()`, `avg(span.duration)`, `p95(span.duration)` -#### Errors Dataset +#### Errors Dataset + - Supports timestamp filters in query - Issue grouping available via `issue` field - Common aggregate functions: `count()`, `count_unique(user.id)`, `last_seen()` #### Logs Dataset + - Does NOT support timestamp filters in query (use `statsPeriod` instead) - Severity levels: fatal, error, warning, info, debug, trace - Common aggregate functions: `count()`, `epm()` #### Metrics Dataset + - Represents newer span metrics, including counters, gauges, and distributions - Uses the same `/events/` endpoint with `dataset=tracemetrics` - Attribute discovery uses `/trace-items/attributes/?itemType=tracemetrics` @@ -125,6 +135,7 @@ https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=ai.m ## Query Syntax ### Basic Filters + - Exact match: `field:value` - Wildcards: `field:*pattern*` - Comparison: `field:>100`, `field:<500` @@ -133,7 +144,9 @@ https://us.sentry.io/api/0/organizations/sentry/events/?dataset=spans&field=ai.m - Attribute existence: `has:field` (recommended for spans) ### Attribute-Based Queries (Recommended for Spans) + Instead of using `span.op` patterns, use `has:` queries for more flexible attribute-based filtering: + - HTTP requests: `has:request.url` instead of `span.op:http*` - Database queries: `has:db.statement` or `has:db.system` instead of `span.op:db*` - AI/LLM calls: `has:ai.model.id` @@ -142,11 +155,13 @@ Instead of using `span.op` patterns, use `has:` queries for more flexible attrib ### Aggregate Functions #### Universal Functions (all datasets) + - `count()` - Count of events - `count_unique(field)` - Count of unique values - `epm()` - Events per minute rate #### Numeric Field Functions (spans, logs) + - `avg(field)` - Average value - `sum(field)` - Sum of values - `min(field)` - Minimum value @@ -154,19 +169,32 @@ Instead of using `span.op` patterns, use `has:` queries for more flexible attrib - `p50(field)`, `p75(field)`, `p90(field)`, `p95(field)`, `p99(field)` - Percentiles #### Tracemetrics Aggregate Functions + - Use the metric-aware form `fn(value,metric.name,metric.type,metric.unit)` - Counter examples: `sum(value,http.server.request.count,counter,-)`, `per_minute(value,http.server.request.count,counter,-)` - Gauge examples: `avg(value,cpu.usage,gauge,percent)`, `max(value,cpu.usage,gauge,percent)` - Distribution examples: `p75(value,http.request.duration,distribution,millisecond)`, `p95(value,http.request.duration,distribution,millisecond)` +#### Conditional Aggregates (`_if`) + +Spans, logs, and metrics append `_if` to an aggregate and take a backtick-wrapped search filter as the first argument. The remaining arguments match the base function. + +- Spans: ``count_if(`span.op:db`)``, ``count_if(`span.duration:>100`)``, ``avg_if(`span.duration:<=100`,span.duration)``, ``p95_if(`has:db.statement`,span.duration)`` +- Logs: ``count_if(`severity:error`)``, ``avg_if(`severity:error`,severity_number)`` +- Metrics: ``sum_if(`environment:production`,value,request_count,counter,-)``, ``p95_if(`release:1.2.3`,value,http.request.duration,distribution,millisecond)`` + +The same form applies to `count_unique_if`, `sum_if`, `min_if`, `max_if`, percentile `_if` functions, and metric `per_second_if` / `per_minute_if`. + #### Errors-Specific Functions -- `count_if(field,equals,value)` - Conditional count + +- `count_if(field,equals,value)` - Conditional count, e.g. `count_if(error.handled,equals,false)`. This three-argument form is errors-only - `last_seen()` - Most recent timestamp - `eps()` - Events per second rate ## Examples ### Find Database Queries (Individual Events) + ``` Query: has:db.statement Fields: ["id", "span.op", "span.description", "span.duration", "transaction", "timestamp", "project", "trace", "db.system", "db.statement"] @@ -175,6 +203,7 @@ Dataset: spans ``` ### Top 10 Slowest API Endpoints (Aggregate) + ``` Query: is_transaction:true Fields: ["transaction", "count()", "avg(span.duration)", "p95(span.duration)"] @@ -183,6 +212,7 @@ Dataset: spans ``` ### Error Count by Type (Aggregate) + ``` Query: level:error Fields: ["error.type", "count()"] @@ -191,6 +221,7 @@ Dataset: errors ``` ### Logs by Severity (Aggregate) + ``` Query: (empty) Fields: ["severity", "count()", "epm()"] @@ -199,6 +230,7 @@ Dataset: logs ``` ### Request Duration Percentiles by Route (Trace Metrics Aggregate) + ``` Query: metric.name:http.request.duration AND metric.type:distribution Fields: ["transaction", "p75(value,http.request.duration,distribution,millisecond)", "p95(value,http.request.duration,distribution,millisecond)", "count(value,http.request.duration,distribution,millisecond)"] @@ -207,6 +239,7 @@ Dataset: metrics ``` ### Tool Calls by Model (Aggregate) + ``` Query: has:gen_ai.tool.name Fields: ["ai.model.id", "gen_ai.tool.name", "count()"] @@ -215,6 +248,7 @@ Dataset: spans ``` ### HTTP Requests (Individual Events) + ``` Query: has:request.url Fields: ["id", "span.op", "span.description", "span.duration", "transaction", "timestamp", "project", "trace", "request.url", "request.method"] @@ -239,6 +273,7 @@ The tool automatically generates shareable Sentry web UI URLs after making API c - **Tracemetrics dataset**: `/organizations/{org}/explore/metrics/` Note: The web UI URLs use different parameter formats than the API: + - Legacy Discover uses simple field parameters - Modern Explore uses dataset-specific parameter formats - The Metrics page uses one or more JSON-encoded `metric=` parameters @@ -258,6 +293,7 @@ The `getEventsExplorerUrl()` method accepts these parameters to determine URL fo 8. **groupByFields**: Array of fields to group by (e.g., `["span.op", "ai.model.id"]`) Based on these parameters: + - If `aggregateFunctions` has items → generates aggregate query URL - For errors dataset → routes to Legacy Discover URL format - For spans/logs datasets → routes to Modern Explore URL format with JSON-encoded `aggregateField` parameters @@ -269,11 +305,13 @@ Based on these parameters: The API and Web UI use different parameter formats: **API (Backend)**: Always uses the same format regardless of dataset + - Endpoint: `/api/0/organizations/{org}/events/` - Parameters: `field`, `query`, `sort`, `dataset`, etc. - Example: `?dataset=spans&field=span.op&field=count()&sort=-count()` **Web UI (Frontend)**: Different formats for different pages + - Legacy Discover: `/organizations/{org}/discover/results/` - Modern Explore: `/organizations/{org}/explore/{dataset}/` - Uses different parameter encoding (e.g., `aggregateField` with JSON for explore pages) @@ -283,6 +321,7 @@ The API and Web UI use different parameter formats: The API **always** uses this format for all datasets: **Individual Events:** + ``` ?dataset=spans &field=id @@ -294,6 +333,7 @@ The API **always** uses this format for all datasets: ``` **Aggregate Queries:** + ``` ?dataset=spans &field=span.op @@ -310,11 +350,13 @@ The only difference between datasets is the `dataset` parameter value and availa All API endpoints support time range filtering using either relative or absolute time parameters: **Relative Time** (`statsPeriod`): + - Format: number + unit (e.g., `1h`, `24h`, `7d`, `30d`) - Default: `14d` (last 14 days) - Example: `?statsPeriod=7d` **Absolute Time** (`start` and `end`): + - Format: ISO 8601 timestamps - Both parameters must be provided together - Example: `?start=2025-06-19T07:00:00&end=2025-06-20T06:59:59` @@ -322,6 +364,7 @@ All API endpoints support time range filtering using either relative or absolute **Important**: Cannot use both `statsPeriod` and `start`/`end` parameters in the same request. **Applies to**: + - Events API: `/organizations/{org}/events/` - Tags API: `/organizations/{org}/tags/` - Trace Items Attributes API: `/organizations/{org}/trace-items/attributes/` @@ -337,6 +380,7 @@ Before translating queries, the tool fetches available attributes/fields for the **Endpoint**: `/api/0/organizations/{org}/tags/` **Parameters**: + - `dataset`: Always `events` for error data - `project`: Numeric project ID (optional) - `statsPeriod`: Time range (e.g., `24h`) @@ -344,11 +388,13 @@ Before translating queries, the tool fetches available attributes/fields for the - `useFlagsBackend`: Set to `1` for latest features **Example**: + ``` https://us.sentry.io/api/0/organizations/sentry/tags/?dataset=events&project=4509062593708032&statsPeriod=24h&useCache=1&useFlagsBackend=1 ``` **Response Format**: + ```json [ { @@ -363,6 +409,7 @@ https://us.sentry.io/api/0/organizations/sentry/tags/?dataset=events&project=450 ``` **Processing**: + - Filters out `sentry:` prefixed tags (internal tags) - Maps to key-value pairs for the AI prompt @@ -371,6 +418,7 @@ https://us.sentry.io/api/0/organizations/sentry/tags/?dataset=events&project=450 **Endpoint**: `/api/0/organizations/{org}/trace-items/attributes/` **Parameters**: + - `itemType`: Either `spans` or `logs` (plural!) - `attributeType`: Optional filter for `string`, `number`, or `boolean`. Omit to return all types. - `project`: Numeric project ID (optional) @@ -379,21 +427,25 @@ https://us.sentry.io/api/0/organizations/sentry/tags/?dataset=events&project=450 **Examples**: Spans string attributes: + ``` https://us.sentry.io/api/0/organizations/sentry/trace-items/attributes/?attributeType=string&itemType=spans&project=4509062593708032&statsPeriod=24h ``` Spans number attributes: + ``` https://us.sentry.io/api/0/organizations/sentry/trace-items/attributes/?attributeType=number&itemType=spans&project=4509062593708032&statsPeriod=24h ``` Logs string attributes: + ``` https://us.sentry.io/api/0/organizations/sentry/trace-items/attributes/?attributeType=string&itemType=logs&project=4509062593708032&statsPeriod=24h ``` **Response Format**: + ```json [ { @@ -423,14 +475,14 @@ const tagsResponse = await apiService.listTags({ dataset: "events", statsPeriod: "14d", useCache: true, - useFlagsBackend: true + useFlagsBackend: true, }); // For spans/logs datasets const attributesResponse = await apiService.listTraceItemAttributes({ organizationSlug, itemType: "spans", // or "logs" - statsPeriod: "14d" + statsPeriod: "14d", }); ``` @@ -442,9 +494,9 @@ After fetching, custom attributes are merged with base fields: ```typescript const allFields = { - ...BASE_COMMON_FIELDS, // Common fields across datasets + ...BASE_COMMON_FIELDS, // Common fields across datasets ...DATASET_FIELDS[dataset], // Dataset-specific fields - ...customAttributes // Organization-specific fields + ...customAttributes, // Organization-specific fields }; ``` @@ -453,6 +505,7 @@ This ensures the AI knows about all available fields when translating queries. ### Error Handling If attribute fetching fails: + - The tool continues with just the base fields - Logs the error for debugging - Does not fail the entire query @@ -500,6 +553,7 @@ The search_events tool handles the complexity of multiple API patterns: ### Response Format Differences **Legacy Discover Response (errors):** + ```json { "data": [ @@ -513,6 +567,7 @@ The search_events tool handles the complexity of multiple API patterns: ``` **EAP Response (spans/logs):** + ```json { "data": [ @@ -528,15 +583,18 @@ The search_events tool handles the complexity of multiple API patterns: ## Troubleshooting ### "Ordered by columns not selected" Error + This occurs when sorting by a field not included in the field list. Ensure your sort field is in the fields array. ### Empty Results + - Check query syntax is valid - Verify time range (`statsPeriod`) - Ensure project has data for the selected dataset - Try broadening the query ### API Errors + - 400: Invalid query syntax or parameters (often due to field mismatch in aggregates) - 404: Project or organization not found - 500: Internal error (check Sentry status) 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 dab182818..6cd23f7f3 100644 --- a/packages/mcp-core/src/tools/support/search-events/config.ts +++ b/packages/mcp-core/src/tools/support/search-events/config.ts @@ -131,6 +131,14 @@ When user asks mathematical questions like "how many X", "total Y used", "sum of - For time-based queries ("today", "yesterday", "this week"), use timeRange parameter - For "total" or "how many" questions: Users typically want highest values first (descending sort) +CONDITIONAL AGGREGATES (_if) FOR SPANS, LOGS, AND METRICS: +- Append _if to an aggregate and pass a backtick-wrapped search filter as the first argument +- Remaining arguments match the base function. The filter uses normal Sentry search syntax +- Spans: count_if(\`span.op:db\`), count_if(\`span.duration:>100\`), avg_if(\`span.duration:<=100\`,span.duration), p95_if(\`has:db.statement\`,span.duration) +- Logs: count_if(\`severity:error\`), avg_if(\`severity:error\`,severity_number) +- Metrics: sum_if(\`environment:production\`,value,my.counter,counter,-), p95_if(\`release:1.2.3\`,value,http.request.duration,distribution,millisecond) +- Errors keeps the three-argument form: count_if(error.handled,equals,false) + DERIVED METRICS AND CALCULATIONS (SPANS ONLY): When user asks for calculated metrics, ratios, or conversions: - Use equation fields with "equation|" prefix @@ -375,6 +383,12 @@ export const DATASET_FIELDS = { "p100(field)": "100th percentile (max), e.g. p100(span.duration)", "epm()": "Events per minute rate", "failure_rate()": "Percentage of failed spans", + "count_if(`query`)": + "Conditional count. First argument is a backtick-wrapped search filter, e.g. count_if(`span.op:db`). Optional second argument defaults to span.duration", + "count_unique_if(`query`,field)": + "Conditional unique count, e.g. count_unique_if(`span.op:db`,user.id)", + "avg_if(`query`,field)": + "Conditional average, e.g. avg_if(`span.duration:<=100`,span.duration). Same backtick-filter form for sum_if, min_if, max_if, and p50_if through p100_if", }, errors: { // Error-specific fields @@ -401,7 +415,7 @@ export const DATASET_FIELDS = { "count()": "Count of error events", "count_unique(field)": "Count of unique values, e.g. count_unique(user.id)", "count_if(field,equals,value)": - "Conditional count, e.g. count_if(error.handled,equals,false)", + "Errors-only conditional count, e.g. count_if(error.handled,equals,false). Spans, logs, and metrics use count_if(`filter`) instead", "last_seen()": "Most recent timestamp of the group", "eps()": "Events per second rate", "epm()": "Events per minute rate", @@ -431,6 +445,12 @@ export const DATASET_FIELDS = { "p99(field)": "99th percentile", "p100(field)": "100th percentile (max)", "epm()": "Events per minute rate", + "count_if(`query`)": + "Conditional count. First argument is a backtick-wrapped search filter, e.g. count_if(`severity:error`)", + "count_unique_if(`query`,field)": + "Conditional unique count, e.g. count_unique_if(`severity:error`,user.id)", + "avg_if(`query`,field)": + "Conditional average, e.g. avg_if(`severity:error`,severity_number). Same backtick-filter form for sum_if, min_if, max_if, and p50_if through p99_if", }, tracemetrics: { "metric.name": "Metric name for the newer span metrics dataset", @@ -470,6 +490,8 @@ export const DATASET_FIELDS = { "Per-second rate for a metric, typically counters", "per_minute(value,metric.name,metric.type,metric.unit)": "Per-minute rate for a metric, typically counters", + "sum_if(`query`,value,metric.name,metric.type,metric.unit)": + "Conditional aggregate. First argument is a backtick-wrapped search filter, then the same metric arguments as the base function, e.g. sum_if(`environment:production`,value,request_count,counter,-). Same form for avg_if, min_if, max_if, count_if, p50_if through p99_if, per_second_if, and per_minute_if", }, profiles: { "profile.id": "Transaction profile ID for a specific captured profile", @@ -638,6 +660,17 @@ export const DATASET_EXAMPLES: Record< sort: "-span.duration", }, }, + { + description: "database span count and p95 duration", + output: { + query: "", + fields: [ + "count_if(`span.op:db`)", + "p95_if(`span.op:db`,span.duration)", + ], + sort: "-count_if(`span.op:db`)", + }, + }, ], errors: [ { @@ -753,6 +786,17 @@ export const DATASET_EXAMPLES: Record< sort: "-timestamp", }, }, + { + description: "error log count and average severity", + output: { + query: "", + fields: [ + "count_if(`severity:error`)", + "avg_if(`severity:error`,severity_number)", + ], + sort: "-count_if(`severity:error`)", + }, + }, ], tracemetrics: [ { @@ -832,6 +876,16 @@ export const DATASET_EXAMPLES: Record< sort: "-p95(value,http.request.duration,distribution,millisecond)", }, }, + { + description: "production request count for a counter metric", + output: { + query: "", + fields: [ + "sum_if(`environment:production`,value,http.server.request.count,counter,-)", + ], + sort: "-sum_if(`environment:production`,value,http.server.request.count,counter,-)", + }, + }, ], profiles: [ {