diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md index 9e2338cfc..99d106f11 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -16,10 +16,10 @@ Query aggregate event data (Explore) Query aggregate event data (Explore) **Flags:** -- `-F, --field ... - API field or aggregate (repeatable). E.g., title, "count()", "p50(transaction.duration)"` +- `-F, --field ... - API field or aggregate (repeatable). E.g., title, "count()", "p50(span.duration)"` - `-m, --metric - Metric name for --dataset metrics. Auto-resolves type/unit via API.` - `--agg - Aggregation for --metric (sum, avg, count, p50, p95, etc.) - (default: "sum")` -- `-d, --dataset - Dataset to query (errors, spans, metrics, logs, replays) - (default: "errors")` +- `-d, --dataset - Dataset to query (errors, spans, metrics, logs, replays; transaction(s) routes to spans) - (default: "errors")` - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort field (prefix with - for desc, e.g., "-count()")` - `-e, --environment ... - Environment filter (repeatable, comma-separated)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md index 53bb750bb..65f0808b0 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md @@ -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:** diff --git a/packages/cli/src/commands/explore.ts b/packages/cli/src/commands/explore.ts index 25d2e0b8f..7b6af3cd0 100644 --- a/packages/cli/src/commands/explore.ts +++ b/packages/cli/src/commands/explore.ts @@ -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"; @@ -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" + ); } /** @@ -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 */ @@ -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(")")); @@ -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. * @@ -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 @@ -592,7 +616,7 @@ function resolveDatasetConfig(params: { fetch: async ({ cursor, limit, timeRange }) => queryEvents(org, { fields: fieldList, - dataset, + dataset: resolved.dataset, query, sort, limit, @@ -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" + @@ -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, }, @@ -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: { diff --git a/packages/cli/src/lib/api/explore.ts b/packages/cli/src/lib/api/explore.ts index d92d2ee5f..0014696ec 100644 --- a/packages/cli/src/lib/api/explore.ts +++ b/packages/cli/src/lib/api/explore.ts @@ -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; @@ -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}). diff --git a/packages/cli/src/lib/api/traces.ts b/packages/cli/src/lib/api/traces.ts index 654cd76bd..00991396b 100644 --- a/packages/cli/src/lib/api/traces.ts +++ b/packages/cli/src/lib/api/traces.ts @@ -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", ]; @@ -395,7 +395,9 @@ 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( @@ -403,7 +405,7 @@ async function fetchTransactionsPage( `/organizations/${orgSlug}/events/`, { params: { - dataset: "transactions", + dataset: "spans", field: TRANSACTION_FIELDS, project: numericProjectId === undefined @@ -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, @@ -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 diff --git a/packages/cli/src/lib/formatters/trace.ts b/packages/cli/src/lib/formatters/trace.ts index c0521791b..6d9394fbd 100644 --- a/packages/cli/src/lib/formatters/trace.ts +++ b/packages/cli/src/lib/formatters/trace.ts @@ -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), ]; } diff --git a/packages/cli/src/lib/hex-id-recovery.ts b/packages/cli/src/lib/hex-id-recovery.ts index ea474cea1..0e3f6df84 100644 --- a/packages/cli/src/lib/hex-id-recovery.ts +++ b/packages/cli/src/lib/hex-id-recovery.ts @@ -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, @@ -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); }; const traceAdapter: FuzzyLookupAdapter = async (ctx) => { diff --git a/packages/cli/src/lib/response-cache.ts b/packages/cli/src/lib/response-cache.ts index 47fe8dfc4..f9246ee78 100644 --- a/packages/cli/src/lib/response-cache.ts +++ b/packages/cli/src/lib/response-cache.ts @@ -76,11 +76,7 @@ const URL_TIER_REGEXPS: Readonly> = { /\/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: [], }; diff --git a/packages/cli/src/types/sentry.ts b/packages/cli/src/types/sentry.ts index 18892020f..56fd80b79 100644 --- a/packages/cli/src/types/sentry.ts +++ b/packages/cli/src/types/sentry.ts @@ -1097,21 +1097,22 @@ export type TraceLogsResponse = InferOutput; // 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")), }), @@ -1120,7 +1121,7 @@ export const TransactionListItemSchema = pipe( export type TransactionListItem = InferOutput; -/** Response from the transactions events endpoint */ +/** Response from the spans events endpoint (is_transaction:true) */ export const TransactionsResponseSchema = object({ data: array(TransactionListItemSchema), meta: optional( diff --git a/packages/cli/test/commands/explore.test.ts b/packages/cli/test/commands/explore.test.ts index d5dc60704..dbdf04766 100644 --- a/packages/cli/test/commands/explore.test.ts +++ b/packages/cli/test/commands/explore.test.ts @@ -185,7 +185,22 @@ const DEFAULT_FLAGS = { fresh: false, }; +const parseExploreDataset = ( + exploreCommand.parameters.flags!.dataset as { + parse: (value: string) => string; + } +).parse; + describe("sentry explore", () => { + describe("dataset parsing", () => { + test.each([ + "transaction", + "transactions", + ])("accepts %s as the legacy transactions view", (dataset) => { + expect(parseExploreDataset(dataset)).toBe(dataset); + }); + }); + describe("target resolution", () => { test("`/` uses org without project filter", async () => { resolveTargetSpy.mockResolvedValue({ org: "my-org" }); @@ -308,7 +323,7 @@ describe("sentry explore", () => { context, { ...DEFAULT_FLAGS, - field: ["transaction", "p50(transaction.duration)"], + field: ["transaction", "p50(span.duration)"], }, "test-org/" ); @@ -316,7 +331,7 @@ describe("sentry explore", () => { expect(queryEventsSpy).toHaveBeenCalledWith( "test-org", expect.objectContaining({ - fields: ["transaction", "p50(transaction.duration)"], + fields: ["transaction", "p50(span.duration)"], }) ); }); @@ -337,6 +352,53 @@ describe("sentry explore", () => { ); }); + test("routes transactions through spans with an automatic filter", async () => { + resolveTargetSpy.mockResolvedValue({ org: "test-org" }); + const { context } = createContext(); + + await func.call( + context, + { + ...DEFAULT_FLAGS, + dataset: "transactions", + query: "environment:production", + }, + "test-org/" + ); + + expect(queryEventsSpy).toHaveBeenCalledWith( + "test-org", + expect.objectContaining({ + dataset: "spans", + fields: ["transaction", "count()"], + query: "environment:production is_transaction:true", + }) + ); + }); + + test("does not duplicate an explicit transaction filter", async () => { + resolveTargetSpy.mockResolvedValue({ org: "test-org", project: "cli" }); + const { context } = createContext(); + + await func.call( + context, + { + ...DEFAULT_FLAGS, + dataset: "transactions", + query: "is_transaction:true", + }, + "test-org/cli" + ); + + expect(queryEventsSpy).toHaveBeenCalledWith( + "test-org", + expect.objectContaining({ + dataset: "spans", + query: "project:cli is_transaction:true", + }) + ); + }); + test("passes user query unchanged when no project filter", async () => { resolveTargetSpy.mockResolvedValue({ org: "test-org" }); const { context } = createContext(); @@ -882,6 +944,25 @@ describe("sentry explore", () => { ); }); + test("preserves transactions in pagination hints", async () => { + resolveTargetSpy.mockResolvedValue({ org: "test-org" }); + queryEventsSpy.mockResolvedValue({ + data: MOCK_EVENTS_RESPONSE, + nextCursor: "cursor123", + }); + const { context, getStdout } = createContext(); + + await func.call( + context, + { ...DEFAULT_FLAGS, dataset: "transactions" }, + "test-org/" + ); + + expect(getStdout()).toContain( + "sentry explore test-org/ -c next --dataset transactions" + ); + }); + test("omits replay default fields from pagination hints", async () => { resolveTargetSpy.mockResolvedValue({ org: "test-org" }); listReplaysSpy.mockResolvedValue({ diff --git a/packages/cli/test/commands/trace/list.test.ts b/packages/cli/test/commands/trace/list.test.ts index 89d58f08f..8c8f3dd6b 100644 --- a/packages/cli/test/commands/trace/list.test.ts +++ b/packages/cli/test/commands/trace/list.test.ts @@ -274,7 +274,7 @@ describe("listCommand.func", () => { id: "evt001", transaction: "GET /api/users", timestamp: "2025-01-30T14:32:15+00:00", - "transaction.duration": 245, + "span.duration": 245, project: "test-project", }, { @@ -282,7 +282,7 @@ describe("listCommand.func", () => { id: "evt002", transaction: "POST /api/checkout", timestamp: "2025-01-30T14:31:00+00:00", - "transaction.duration": 1823, + "span.duration": 1823, project: "test-project", }, ]; diff --git a/packages/cli/test/fixtures/transactions.json b/packages/cli/test/fixtures/transactions.json index 26272912d..e9705a8cc 100644 --- a/packages/cli/test/fixtures/transactions.json +++ b/packages/cli/test/fixtures/transactions.json @@ -2,18 +2,18 @@ "data": [ { "trace": "aaaa1111bbbb2222cccc3333dddd4444", - "id": "evt001evt001evt001evt001evt00101", + "id": "a1b2c3d4e5f60718", "transaction": "GET /api/users", "timestamp": "2025-01-30T14:32:15+00:00", - "transaction.duration": 245, + "span.duration": 245, "project": "test-project" }, { "trace": "eeee5555ffff6666aaaa7777bbbb8888", - "id": "evt002evt002evt002evt002evt00202", + "id": "b2c3d4e5f6071829", "transaction": "POST /api/checkout", "timestamp": "2025-01-30T14:31:00+00:00", - "transaction.duration": 1823, + "span.duration": 1823, "project": "test-project" } ], @@ -23,7 +23,7 @@ "id": "string", "transaction": "string", "timestamp": "date", - "transaction.duration": "duration", + "span.duration": "duration", "project": "string" } } diff --git a/packages/cli/test/lib/api-client.coverage.test.ts b/packages/cli/test/lib/api-client.coverage.test.ts index ce059a137..c15ced08e 100644 --- a/packages/cli/test/lib/api-client.coverage.test.ts +++ b/packages/cli/test/lib/api-client.coverage.test.ts @@ -1706,7 +1706,7 @@ describe("traces.ts (transactions)", () => { id: "evt-1", transaction: "GET /api/users", timestamp: "2024-01-01T00:00:00Z", - "transaction.duration": 150, + "span.duration": 150, project: "test-project", }, ], @@ -1717,7 +1717,8 @@ describe("traces.ts (transactions)", () => { const req = new Request(input!, init); const url = new URL(req.url); expect(url.pathname).toContain("/organizations/test-org/events/"); - expect(url.searchParams.get("dataset")).toBe("transactions"); + expect(url.searchParams.get("dataset")).toBe("spans"); + expect(url.searchParams.get("query")).toContain("is_transaction:true"); expect(url.searchParams.get("query")).toContain("project:test-project"); return new Response(JSON.stringify(txnResponse), { status: 200, @@ -1763,7 +1764,7 @@ describe("traces.ts (transactions)", () => { globalThis.fetch = mockFetch(async (input, init) => { const req = new Request(input!, init); const url = new URL(req.url); - expect(url.searchParams.get("sort")).toBe("-transaction.duration"); + expect(url.searchParams.get("sort")).toBe("-span.duration"); expect(url.searchParams.get("statsPeriod")).toBe("24h"); return new Response(JSON.stringify(txnResponse), { status: 200, diff --git a/packages/cli/test/lib/api-client.test.ts b/packages/cli/test/lib/api-client.test.ts index a50479197..e8130673b 100644 --- a/packages/cli/test/lib/api-client.test.ts +++ b/packages/cli/test/lib/api-client.test.ts @@ -1426,7 +1426,7 @@ describe("listTransactions", () => { id: "evt001", transaction: "GET /api/users", timestamp: "2025-01-30T14:32:15+00:00", - "transaction.duration": 245, + "span.duration": 245, project: "my-project", }, ], @@ -1501,9 +1501,10 @@ describe("listTransactions", () => { const url = new URL(capturedUrl); expect(url.searchParams.get("cursor")).toBe("1735689600:0:0"); - expect(url.searchParams.get("sort")).toBe("-transaction.duration"); + expect(url.searchParams.get("sort")).toBe("-span.duration"); expect(url.searchParams.get("per_page")).toBe("50"); expect(url.searchParams.get("query")).toContain("transaction:GET"); + expect(url.searchParams.get("query")).toContain("is_transaction:true"); }); test("uses project query param for numeric project IDs", async () => { @@ -1525,9 +1526,9 @@ describe("listTransactions", () => { const url = new URL(capturedUrl); expect(url.searchParams.get("project")).toBe("12345"); - // Should NOT include project:12345 in the query + // Query should only carry the transaction filter, not project scoping const query = url.searchParams.get("query"); - expect(query).toBeNull(); + expect(query).toBe("is_transaction:true"); }); test("uses project:slug in query for non-numeric project slugs", async () => { diff --git a/packages/cli/test/lib/api/traces.test.ts b/packages/cli/test/lib/api/traces.test.ts index 5abcad065..9a18b0144 100644 --- a/packages/cli/test/lib/api/traces.test.ts +++ b/packages/cli/test/lib/api/traces.test.ts @@ -79,7 +79,7 @@ describe("listTransactions", () => { id: "string", transaction: "string", timestamp: "date", - "transaction.duration": "duration", + "span.duration": "duration", project: "string", }, }; @@ -91,7 +91,7 @@ describe("listTransactions", () => { id: `id-${i}`, transaction: `/api/endpoint-${i}`, timestamp: "2024-01-15T00:00:00Z", - "transaction.duration": 100 + i, + "span.duration": 100 + i, project: "my-project", })); } @@ -105,12 +105,13 @@ describe("listTransactions", () => { expect(capturedUrl).toContain("/api/0/organizations/my-org/events/"); }); - test("sends dataset=transactions", async () => { + test("sends dataset=spans with is_transaction:true", async () => { mockOk({ data: [], meta: TX_META }); await listTransactions("my-org", "my-project"); - expect(capturedUrl).toContain("dataset=transactions"); + expect(capturedUrl).toContain("dataset=spans"); + expect(decodeURIComponent(capturedUrl)).toContain("is_transaction:true"); }); test("passes per_page capped at 100 even when limit is higher", async () => { @@ -144,14 +145,12 @@ describe("listTransactions", () => { expect(capturedUrl).toContain(`sort=${encodeURIComponent("-timestamp")}`); }); - test('sends sort=-transaction.duration for sort="duration"', async () => { + test('sends sort=-span.duration for sort="duration"', async () => { mockOk({ data: [], meta: TX_META }); await listTransactions("my-org", "my-project", { sort: "duration" }); - expect(decodeURIComponent(capturedUrl)).toContain( - "sort=-transaction.duration" - ); + expect(decodeURIComponent(capturedUrl)).toContain("sort=-span.duration"); }); test("passes cursor when provided", async () => { @@ -271,8 +270,8 @@ describe("listTransactions", () => { await listTransactions("my-org", "my-project"); - expect(decodeURIComponent(capturedUrl)).toContain( - "query=project:my-project" + expect(decodeURIComponent(capturedUrl).replaceAll("+", " ")).toContain( + "query=is_transaction:true project:my-project" ); // Should NOT appear as a separate project= param expect(capturedUrl).not.toMatch(/[?&]project=my-project/); diff --git a/packages/cli/test/lib/formatters/trace.property.test.ts b/packages/cli/test/lib/formatters/trace.property.test.ts index 36fb9ca2c..585f39cb7 100644 --- a/packages/cli/test/lib/formatters/trace.property.test.ts +++ b/packages/cli/test/lib/formatters/trace.property.test.ts @@ -70,7 +70,7 @@ const transactionItemArb = record({ id: hexId32Arb, transaction: transactionNameArb, timestamp: isoTimestampArb, - "transaction.duration": positiveDurationArb, + "span.duration": positiveDurationArb, project: slugArb, }) as unknown as import("fast-check").Arbitrary; diff --git a/packages/cli/test/lib/formatters/trace.test.ts b/packages/cli/test/lib/formatters/trace.test.ts index 92e8d7363..5e1296bc5 100644 --- a/packages/cli/test/lib/formatters/trace.test.ts +++ b/packages/cli/test/lib/formatters/trace.test.ts @@ -92,7 +92,7 @@ function makeTransaction( id: "b".repeat(32), transaction: "GET /api/users", timestamp: "2025-01-15T10:30:00Z", - "transaction.duration": 1234, + "span.duration": 1234, project: "my-project", ...overrides, }; @@ -160,9 +160,7 @@ describe("formatTraceRow (rendered mode)", () => { }); test("includes formatted duration", () => { - const row = formatTraceRow( - makeTransaction({ "transaction.duration": 245 }) - ); + const row = formatTraceRow(makeTransaction({ "span.duration": 245 })); expect(row).toContain("245ms"); }); @@ -208,9 +206,7 @@ describe("formatTraceRow (plain mode)", () => { }); test("includes formatted duration", () => { - const row = formatTraceRow( - makeTransaction({ "transaction.duration": 245 }) - ); + const row = formatTraceRow(makeTransaction({ "span.duration": 245 })); expect(row).toContain("245ms"); }); @@ -423,7 +419,7 @@ describe("formatTraceTable", () => { test("includes formatted durations", () => { const result = stripAnsi( - formatTraceTable([makeTransaction({ "transaction.duration": 1500 })]) + formatTraceTable([makeTransaction({ "span.duration": 1500 })]) ); expect(result).toContain("1.50s"); }); diff --git a/packages/cli/test/lib/hex-id-recovery.adapters.test.ts b/packages/cli/test/lib/hex-id-recovery.adapters.test.ts index b53bbabfb..05d1ea260 100644 --- a/packages/cli/test/lib/hex-id-recovery.adapters.test.ts +++ b/packages/cli/test/lib/hex-id-recovery.adapters.test.ts @@ -94,7 +94,7 @@ describe("adapter context guards", () => { // --------------------------------------------------------------------------- describe("adapter query params", () => { - test("event adapter queries the transactions dataset with project scope", async () => { + test("event adapter queries the errors dataset with project scope", async () => { let queryUrl = ""; globalThis.fetch = mockFetch(async (input, init) => { const req = new Request(input!, init); @@ -102,19 +102,11 @@ describe("adapter query params", () => { return eventsResponse([ { id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - trace: "tr1", - transaction: "/api/test1", timestamp: "2026-01-01T00:00:00Z", - "transaction.duration": 42, - project: "test-project", }, { id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - trace: "tr2", - transaction: "/api/test2", timestamp: "2026-01-01T00:00:00Z", - "transaction.duration": 17, - project: "test-project", }, ]); }); @@ -124,8 +116,10 @@ describe("adapter query params", () => { project: "test-project", }); - expect(queryUrl).toContain("dataset=transactions"); + expect(queryUrl).toContain("dataset=errors"); + expect(queryUrl).toContain("field=id"); expect(queryUrl).toContain("project%3Atest-project"); // project:test-project URL-encoded + expect(queryUrl).toContain("sort=-timestamp"); expect(queryUrl).toContain("statsPeriod=90d"); expect(ids).toEqual([ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", diff --git a/packages/cli/test/lib/response-cache.property.test.ts b/packages/cli/test/lib/response-cache.property.test.ts index 834297657..74776715e 100644 --- a/packages/cli/test/lib/response-cache.property.test.ts +++ b/packages/cli/test/lib/response-cache.property.test.ts @@ -211,9 +211,9 @@ describe("property: classifyUrl", () => { expect(classifyUrl(url)).toBe("volatile"); }); - test("dataset=transactions URLs are volatile", () => { + test("dataset=spans URLs are volatile", () => { const url = - "https://us.sentry.io/api/0/organizations/org/events/?dataset=transactions"; + "https://us.sentry.io/api/0/organizations/org/events/?dataset=spans"; expect(classifyUrl(url)).toBe("volatile"); }); diff --git a/packages/cli/test/mocks/routes.ts b/packages/cli/test/mocks/routes.ts index db06122df..f6162749e 100644 --- a/packages/cli/test/mocks/routes.ts +++ b/packages/cli/test/mocks/routes.ts @@ -149,6 +149,31 @@ function issueIndexResponse( return { body: issuesFixture }; } +/** + * Resolve the events-endpoint body for the logs dataset. + * + * If the query contains a sentry.item_id filter, returns the matching detailed + * log. Query format: "project:${proj} sentry.item_id:${id}" or + * "project:${proj} sentry.item_id:[id1,id2,...]" + */ +function resolveLogEventsBody(query: string | null) { + if (query?.includes("sentry.item_id:")) { + const bracketMatch = query.match(/sentry\.item_id:\[([^\]]+)\]/); + const singleMatch = query.match(/sentry\.item_id:([0-9a-f]{32})/i); + let ids: string[] = []; + if (bracketMatch) { + ids = bracketMatch[1].split(",").map((s) => s.trim()); + } else if (singleMatch) { + ids = [singleMatch[1]]; + } + if (ids.includes(TEST_LOG_ID)) { + return logDetailFixture; + } + return { data: [], meta: { fields: {} } }; + } + return logsFixture; +} + export const apiRoutes: MockRoute[] = [ // User Regions (multi-region support) // Returns the mock server itself as the only region @@ -393,7 +418,7 @@ export const apiRoutes: MockRoute[] = [ }, }, - // Logs & Transactions (Events API - dispatches on dataset param) + // Logs & trace list (Events API - dispatches on dataset param) { method: "GET", path: "/api/0/organizations/:orgSlug/events/", @@ -401,35 +426,14 @@ export const apiRoutes: MockRoute[] = [ if (params.orgSlug === TEST_ORG) { const url = new URL(req.url); const dataset = url.searchParams.get("dataset"); - - // Transactions dataset (trace list) - if (dataset === "transactions") { - return { body: transactionsFixture }; - } - - // Logs dataset (default) const query = url.searchParams.get("query"); - // If query contains sentry.item_id filter, return detailed log - // Query format: "project:${proj} sentry.item_id:${id}" or - // "project:${proj} sentry.item_id:[id1,id2,...]" - if (query?.includes("sentry.item_id:")) { - // Extract IDs from both single and bracket syntax - const bracketMatch = query.match(/sentry\.item_id:\[([^\]]+)\]/); - const singleMatch = query.match(/sentry\.item_id:([0-9a-f]{32})/i); - let ids: string[] = []; - if (bracketMatch) { - ids = bracketMatch[1].split(",").map((s) => s.trim()); - } else if (singleMatch) { - ids = [singleMatch[1]]; - } - - if (ids.includes(TEST_LOG_ID)) { - return { body: logDetailFixture }; - } - // Return empty data for non-existent log - return { body: { data: [], meta: { fields: {} } } }; + if ( + dataset === "spans" && + (query ?? "").includes("is_transaction:true") + ) { + return { body: transactionsFixture }; } - return { body: logsFixture }; + return { body: resolveLogEventsBody(query) }; } return { status: 404, body: notFoundFixture }; },