Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Query aggregate event data (Explore)
Query aggregate event data (Explore)

**Flags:**
- `-F, --field <value>... - API field or aggregate (repeatable). E.g., title, "count()", "p50(transaction.duration)"`
- `-F, --field <value>... - API field or aggregate (repeatable). E.g., title, "count()", "p50(span.duration)"`
- `-m, --metric <value> - Metric name for --dataset metrics. Auto-resolves type/unit via API.`
- `--agg <value> - Aggregation for --metric (sum, avg, count, p50, p95, etc.) - (default: "sum")`
- `-d, --dataset <value> - Dataset to query (errors, spans, metrics, logs, replays) - (default: "errors")`
- `-d, --dataset <value> - Dataset to query (errors, spans, metrics, logs, replays; transaction(s) routes to spans) - (default: "errors")`
- `-q, --query <value> - Search query (Sentry search syntax)`
- `-s, --sort <value> - Sort field (prefix with - for desc, e.g., "-count()")`
- `-e, --environment <value>... - Environment filter (repeatable, comma-separated)`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ List recent traces in a project
| Field | Type | Description |
|-------|------|-------------|
| `trace` | string | Trace ID |
| `id` | string | Event ID |
| `id` | string | Span ID of the root span |
| `transaction` | string | Transaction name |
| `timestamp` | string | Timestamp (ISO 8601) |
| `transaction.duration` | number | Duration (ms) |
| `span.duration` | number | Duration (ms) |
| `project` | string | Project slug |

**Examples:**
Expand Down
52 changes: 38 additions & 14 deletions packages/cli/src/commands/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ const log = logger.withTag("explore");

/** Default fields when none specified — top errors view */
const DEFAULT_FIELDS = ["title", "count()"];
const DEFAULT_TRANSACTION_FIELDS = ["transaction", "count()"];
const LEGACY_TRANSACTION_DATASETS = new Set(["transaction", "transactions"]);
const IS_TRANSACTION_FILTER = "is_transaction:true";
const IS_TRANSACTION_FILTER_PATTERN = /(?:^|\s)is_transaction:true(?:\s|$)/;

/** Default dataset */
const DEFAULT_DATASET = "errors";
Expand Down Expand Up @@ -162,13 +166,13 @@ type ExploreData = {
function parseDataset(value: string): string {
const lower = value.toLowerCase();
const resolved = DATASET_ALIASES[lower];
if (!resolved) {
throw new ValidationError(
`Invalid dataset "${value}". Must be one of: ${[...VALID_DATASETS].join(", ")}`,
"dataset"
);
if (resolved || LEGACY_TRANSACTION_DATASETS.has(lower)) {
return resolved ?? lower;
}
return resolved;
throw new ValidationError(
`Invalid dataset "${value}". Must be one of: ${Array.from(VALID_DATASETS).join(", ")}`,
"dataset"
);
}

/**
Expand Down Expand Up @@ -301,7 +305,13 @@ function jsonTransformExplore(data: ExploreData, fields?: string[]): unknown {
const DEFAULT_LIMIT = 25;

function defaultFieldsForDataset(dataset: string): readonly string[] {
return dataset === "replays" ? DEFAULT_REPLAY_EXPLORE_FIELDS : DEFAULT_FIELDS;
if (dataset === "replays") {
return DEFAULT_REPLAY_EXPLORE_FIELDS;
}
if (LEGACY_TRANSACTION_DATASETS.has(dataset)) {
return DEFAULT_TRANSACTION_FIELDS;
}
return DEFAULT_FIELDS;
}

/** Append --metric / --agg flags to hint parts */
Expand Down Expand Up @@ -380,7 +390,7 @@ function appendFlagHints(

/**
* Detect the first aggregate function in the field list.
* Aggregates contain parentheses, e.g., `count()`, `p50(transaction.duration)`.
* Aggregates contain parentheses, e.g., `count()`, `p50(span.duration)`.
*/
function findFirstAggregate(fieldList: string[]): string | undefined {
return fieldList.find((f) => f.includes("(") && f.includes(")"));
Expand Down Expand Up @@ -497,6 +507,19 @@ function buildEnvironmentQuery(
return `environment:[${environment.join(",")}]`;
}

function resolveEventsDataset(dataset: string, query: string | undefined) {
if (!LEGACY_TRANSACTION_DATASETS.has(dataset)) {
return { dataset, query };
}
if (IS_TRANSACTION_FILTER_PATTERN.test(query ?? "")) {
return { dataset: "spans", query };
}
return {
dataset: "spans",
query: [query, IS_TRANSACTION_FILTER].filter(Boolean).join(" "),
};
}

/**
* Resolve dataset-specific configuration: sort, query, validation, and fetch.
*
Expand Down Expand Up @@ -560,13 +583,14 @@ function resolveDatasetConfig(params: {
// Non-replay datasets: translate --environment into query filter terms
// since the Discover/Events API expects environment:... in the query string.
const envPrefix = buildEnvironmentQuery(environment);
const resolved = resolveEventsDataset(dataset, flags.query);
const queryWithEnv =
[envPrefix, flags.query].filter(Boolean).join(" ") || undefined;
[envPrefix, resolved.query].filter(Boolean).join(" ") || undefined;

const firstAgg = findFirstAggregate(fieldList);
const rawSort = flags.sort ?? (firstAgg ? `-${firstAgg}` : undefined);
let sort: string | undefined;
if (SORTABLE_DATASETS.has(dataset)) {
if (SORTABLE_DATASETS.has(resolved.dataset)) {
// A deterministic sort is required for correct cursor pagination: the
// events cursor is offset-based, so without a stable total order the
// separate page requests overlap and skip rows, producing duplicate and
Expand All @@ -592,7 +616,7 @@ function resolveDatasetConfig(params: {
fetch: async ({ cursor, limit, timeRange }) =>
queryEvents(org, {
fields: fieldList,
dataset,
dataset: resolved.dataset,
query,
sort,
limit,
Expand Down Expand Up @@ -624,7 +648,7 @@ export const exploreCommand = buildListCommand("explore", {
fullDescription:
"Query the Sentry Explore API for aggregate event data.\n\n" +
"Supports arbitrary fields including columns (title, project),\n" +
"aggregates (count(), count_unique(user), p50(transaction.duration)),\n" +
"aggregates (count(), count_unique(user), p50(span.duration)),\n" +
"and equations. Results are returned as a table.\n\n" +
"Datasets:\n" +
" errors Error events (default)\n" +
Expand Down Expand Up @@ -672,7 +696,7 @@ export const exploreCommand = buildListCommand("explore", {
kind: "parsed",
parse: String,
brief:
'API field or aggregate (repeatable). E.g., title, "count()", "p50(transaction.duration)"',
'API field or aggregate (repeatable). E.g., title, "count()", "p50(span.duration)"',
variadic: true,
optional: true,
},
Expand All @@ -692,7 +716,7 @@ export const exploreCommand = buildListCommand("explore", {
dataset: {
kind: "parsed",
parse: parseDataset,
brief: `Dataset to query (${[...VALID_DATASETS].join(", ")})`,
brief: `Dataset to query (${[...VALID_DATASETS].join(", ")}; transaction(s) routes to spans)`,
default: DEFAULT_DATASET,
},
query: {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/api/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
export type ExploreQueryOptions = {
/** Fields to request — columns, aggregates, or equations */
fields: string[];
/** Dataset to query: errors, transactions, spans, discover */
/** Dataset to query: errors, spans, tracemetrics, logs, replays */
dataset?: string;
/** Sentry search query filter */
query?: string;
Expand Down Expand Up @@ -159,7 +159,7 @@ export async function queryMetricsMeta(
*
* Calls `GET /organizations/{org}/events/` with the specified fields, dataset,
* query, sort, and time range. Supports all standard Sentry Explore fields
* including aggregates like `count()`, `count_unique(user)`, `p50(transaction.duration)`.
* including aggregates like `count()`, `count_unique(user)`, `p50(span.duration)`.
*
* When `limit` exceeds {@link API_MAX_PER_PAGE}, transparently fetches multiple
* pages using cursor-based pagination (bounded by {@link MAX_PAGINATION_PAGES}).
Expand Down
18 changes: 9 additions & 9 deletions packages/cli/src/lib/api/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,13 @@ export function normalizeTraceSpan(span: TraceSpan): TraceSpan {
return normalized;
}

/** Fields to request from the transactions API */
/** Fields to request when listing transactions (root spans) from the spans dataset */
const TRANSACTION_FIELDS = [
"trace",
"id",
"transaction",
"timestamp",
"transaction.duration",
"span.duration",
"project",
];

Expand Down Expand Up @@ -395,15 +395,17 @@ async function fetchTransactionsPage(
);
const projectFilter =
numericProjectId === undefined ? `project:${projectSlug}` : "";
const fullQuery = [projectFilter, options.query].filter(Boolean).join(" ");
const fullQuery = ["is_transaction:true", projectFilter, options.query]
.filter(Boolean)
.join(" ");

const { data: response, headers } =
await apiRequestToRegion<TransactionsResponse>(
regionUrl,
`/organizations/${orgSlug}/events/`,
{
params: {
dataset: "transactions",
dataset: "spans",
field: TRANSACTION_FIELDS,
project:
numericProjectId === undefined
Expand All @@ -420,10 +422,7 @@ async function fetchTransactionsPage(
: (options.statsPeriod ?? "7d"),
start: options.start,
end: options.end,
sort:
options.sort === "duration"
? "-transaction.duration"
: "-timestamp",
sort: options.sort === "duration" ? "-span.duration" : "-timestamp",
cursor: options.cursor,
},
schema: TransactionsResponseSchema,
Expand All @@ -436,7 +435,8 @@ async function fetchTransactionsPage(

/**
* List recent transactions for a project.
* Uses the Explore/Events API with dataset=transactions.
* Uses the Explore/Events API with dataset=spans and an `is_transaction:true`
* filter — transactions are root spans in EAP storage.
*
* Handles project slug vs numeric ID automatically:
* - Numeric IDs (or `options.projectId`) are passed as the `project` parameter
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/formatters/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function buildTraceRowCells(
return [
`\`${item.trace}\``,
escapeMarkdownCell(item.transaction || "unknown"),
formatTraceDuration(item["transaction.duration"]),
formatTraceDuration(item["span.duration"]),
formatRelativeTime(item.timestamp),
];
}
Expand Down
13 changes: 8 additions & 5 deletions packages/cli/src/lib/hex-id-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@

import { addBreadcrumb } from "@sentry/node-core/light";

import type { SpanListItem, TransactionListItem } from "../types/index.js";
import { listLogs, listSpans, listTransactions } from "./api-client.js";
import type { SpanListItem } from "../types/index.js";
import { listLogs, listSpans, queryEvents } from "./api-client.js";
import {
type ParsedOrgProject,
ProjectSpecificationType,
Expand Down Expand Up @@ -452,12 +452,15 @@ const eventAdapter: FuzzyLookupAdapter = async (ctx) => {
if (!(ctx.org && ctx.project)) {
return [];
}
const { data } = await listTransactions(ctx.org, ctx.project, {
const { data } = await queryEvents(ctx.org, {
dataset: "errors",
fields: ["id"],
query: `project:${ctx.project}`,
limit: SCAN_LIMIT,
sort: "-timestamp",
statsPeriod: ctx.period ?? SCAN_PERIODS.event,
sort: "date",
});
return (data as TransactionListItem[]).map((t) => t.id);
return data.data.map((row) => String(row.id ?? "")).filter(Boolean);
Comment thread
cursor[bot] marked this conversation as resolved.
};

const traceAdapter: FuzzyLookupAdapter = async (ctx) => {
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/lib/response-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,7 @@ const URL_TIER_REGEXPS: Readonly<Record<TtlTier, readonly RegExp[]>> = {
/\/trace-items\/[0-9a-f]+\//,
],
// Issue endpoints (lists AND detail views), dataset queries, trace-logs
volatile: [
/\/issues\//,
/[?&]dataset=(?:logs|transactions)/,
/\/trace-logs\//,
],
volatile: [/\/issues\//, /[?&]dataset=(?:logs|spans)/, /\/trace-logs\//],
// Default fallback — no patterns needed
stable: [],
};
Expand Down
13 changes: 7 additions & 6 deletions packages/cli/src/types/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1097,21 +1097,22 @@ export type TraceLogsResponse = InferOutput<typeof TraceLogsResponseSchema>;
// Transaction (for trace listing)

/**
* Transaction list item from the Explore/Events API (dataset=transactions).
* Transaction list item from the Explore/Events API (spans dataset,
* `is_transaction:true` — transactions are root spans).
* Fields match the response when querying trace, id, transaction, timestamp, etc.
*/
export const TransactionListItemSchema = pipe(
looseObject({
/** Trace ID this transaction belongs to */
trace: pipe(string(), description("Trace ID")),
/** Event ID of the transaction */
id: pipe(string(), description("Event ID")),
/** Span ID of the root (transaction) span */
id: pipe(string(), description("Span ID of the root span")),
/** Transaction name (e.g., "GET /api/users") */
transaction: pipe(string(), description("Transaction name")),
/** ISO timestamp of the transaction */
timestamp: pipe(string(), description("Timestamp (ISO 8601)")),
/** Transaction duration in milliseconds */
"transaction.duration": pipe(number(), description("Duration (ms)")),
/** Duration of the root span in milliseconds */
"span.duration": pipe(number(), description("Duration (ms)")),
/** Project slug */
project: pipe(string(), description("Project slug")),
}),
Expand All @@ -1120,7 +1121,7 @@ export const TransactionListItemSchema = pipe(

export type TransactionListItem = InferOutput<typeof TransactionListItemSchema>;

/** Response from the transactions events endpoint */
/** Response from the spans events endpoint (is_transaction:true) */
export const TransactionsResponseSchema = object({
data: array(TransactionListItemSchema),
meta: optional(
Expand Down
Loading
Loading