Skip to content
Open
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
9 changes: 8 additions & 1 deletion packages/cli/src/commands/span/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ import {
TARGET_PATTERN_NOTE,
} from "../../lib/list-command.js";
import { withProgress } from "../../lib/polling.js";
import { resolveOrgProjectFromArg } from "../../lib/resolve-target.js";
import {
resolveLogProjectId,
resolveOrgProjectFromArg,
} from "../../lib/resolve-target.js";
import { sanitizeQuery } from "../../lib/search-query.js";
import {
appendPeriodHint,
Expand Down Expand Up @@ -452,6 +455,9 @@ async function handleProjectMode(
cwd,
COMMAND_NAME
);
// Resolve slug → numeric ID so the Events query scopes via the `project`
// param. `project:<slug>` only matches actively-selected projects (#1317).
const projectId = await resolveLogProjectId(org, project);
const apiQuery = flags.query ? translateSpanQuery(flags.query) : undefined;

const contextKey = buildPaginationContextKey(
Expand All @@ -473,6 +479,7 @@ async function handleProjectMode(
sort: flags.sort,
limit: flags.limit,
cursor,
projectId,
...timeRangeToApiParams(timeRange),
extraFields: extraApiFields,
}).catch((error: unknown): never => {
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/commands/trace/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import {
TARGET_PATTERN_NOTE,
} from "../../lib/list-command.js";
import { withProgress } from "../../lib/polling.js";
import { resolveOrgProjectFromArg } from "../../lib/resolve-target.js";
import {
resolveLogProjectId,
resolveOrgProjectFromArg,
} from "../../lib/resolve-target.js";
import { sanitizeQuery } from "../../lib/search-query.js";
import {
appendPeriodHint,
Expand Down Expand Up @@ -266,6 +269,10 @@ export const listCommand = buildListCommand("trace", {
cwd,
COMMAND_NAME
);
// Resolve slug → numeric ID so the Events query scopes via the `project`
// param. `project:<slug>` only matches actively-selected projects and can
// otherwise 400 with "not actively selected" (#1317).
const projectId = await resolveLogProjectId(org, project);
// Build context key and resolve cursor for pagination
const contextKey = buildPaginationContextKey("trace", `${org}/${project}`, {
sort: flags.sort,
Expand All @@ -289,6 +296,7 @@ export const listCommand = buildListCommand("trace", {
limit: flags.limit,
sort: flags.sort,
cursor,
projectId,
...timeRangeToApiParams(timeRange),
}).catch((error: unknown): never => {
// An unparseable user --query is a user input mistake, not a CLI bug.
Expand Down
71 changes: 55 additions & 16 deletions packages/cli/src/lib/api/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,24 @@ const TRANSACTION_FIELDS = [
"project",
];

/**
* Resolve the numeric project ID to send via the `project` query param.
*
* Prefers an explicit `projectId` from the caller; otherwise falls back to the
* slug when it is itself all-digits (a numeric project ID passed as the slug).
* Returns `undefined` when neither yields a numeric ID, signalling that the
* caller should scope via `project:<slug>` search syntax instead.
*/
function resolveNumericProjectId(
projectSlug: string,
projectId: number | undefined
): number | undefined {
if (projectId !== undefined) {
return projectId;
}
return isAllDigits(projectSlug) ? Number(projectSlug) : undefined;
}

type ListTransactionsOptions = {
/** Search query using Sentry query syntax */
query?: string;
Expand All @@ -347,6 +365,12 @@ type ListTransactionsOptions = {
start?: string;
/** Absolute end datetime (ISO-8601). Mutually exclusive with statsPeriod. */
end?: string;
/**
* Numeric project ID. When provided, uses the `project` query param instead
* of `project:<slug>` search syntax, avoiding "not actively selected" errors
* (same class of bug as #1317 / #312).
*/
projectId?: number;
};

/**
Expand All @@ -363,8 +387,14 @@ async function fetchTransactionsPage(
options: ListTransactionsOptions,
perPage: number
): Promise<PaginatedResponse<TransactionListItem[]>> {
const isNumericProject = isAllDigits(projectSlug);
const projectFilter = isNumericProject ? "" : `project:${projectSlug}`;
// Prefer the numeric `project=` param — `project:<slug>` in the search query
// only matches projects that are actively selected (#1317).
const numericProjectId = resolveNumericProjectId(
projectSlug,
options.projectId
);
const projectFilter =
numericProjectId === undefined ? `project:${projectSlug}` : "";
const fullQuery = [projectFilter, options.query].filter(Boolean).join(" ");

const { data: response, headers } =
Expand All @@ -375,7 +405,10 @@ async function fetchTransactionsPage(
params: {
dataset: "transactions",
field: TRANSACTION_FIELDS,
project: isNumericProject ? projectSlug : undefined,
project:
numericProjectId === undefined
? undefined
: String(numericProjectId),
// Convert empty string to undefined so ky omits the param entirely;
// sending `query=` causes the Sentry API to behave differently than
// omitting the parameter.
Expand Down Expand Up @@ -406,8 +439,8 @@ async function fetchTransactionsPage(
* Uses the Explore/Events API with dataset=transactions.
*
* Handles project slug vs numeric ID automatically:
* - Numeric IDs are passed as the `project` parameter
* - Slugs are added to the query string as `project:{slug}`
* - Numeric IDs (or `options.projectId`) are passed as the `project` parameter
* - Slugs fall back to `project:{slug}` in the query string when no ID is known
*
* When `limit` exceeds {@link API_MAX_PER_PAGE}, transparently fetches multiple
* pages using cursor-based pagination (bounded by {@link MAX_PAGINATION_PAGES}).
Expand Down Expand Up @@ -477,6 +510,12 @@ type ListSpansOptions = {
end?: string;
/** When true, search across all projects (sends project=-1). Used for trace mode. */
allProjects?: boolean;
/**
* Numeric project ID. When provided (and not `allProjects`), uses the
* `project` query param instead of `project:<slug>` search syntax, avoiding
* "not actively selected" errors (same class of bug as #1317 / #312).
*/
projectId?: number;
};

/**
Expand All @@ -493,15 +532,15 @@ async function fetchSpansPage(
options: ListSpansOptions,
perPage: number
): Promise<PaginatedResponse<SpanListItem[]>> {
const isNumericProject = isAllDigits(projectSlug);
let projectFilter: string;
if (options.allProjects) {
projectFilter = "";
} else if (isNumericProject) {
projectFilter = "";
} else {
projectFilter = `project:${projectSlug}`;
}
// Prefer the numeric `project=` param — `project:<slug>` in the search query
// only matches projects that are actively selected (#1317).
const numericProjectId = options.allProjects
? undefined
: resolveNumericProjectId(projectSlug, options.projectId);
const projectFilter =
options.allProjects || numericProjectId !== undefined
? ""
: `project:${projectSlug}`;
const fullQuery = [projectFilter, options.query].filter(Boolean).join(" ");

const fields = options.extraFields?.length
Expand All @@ -511,8 +550,8 @@ async function fetchSpansPage(
let projectParam: string | undefined;
if (options.allProjects) {
projectParam = "-1";
} else if (isNumericProject) {
projectParam = projectSlug;
} else if (numericProjectId !== undefined) {
projectParam = String(numericProjectId);
}

const { data: response, headers } = await apiRequestToRegion<SpansResponse>(
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/resolve-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,9 +952,9 @@ export async function fetchProjectId(
}

/**
* Resolve a project slug to its numeric ID for log queries, tolerating failures.
* Resolve a project slug to its numeric ID for Events API queries, tolerating failures.
*
* Log listing and lookup scope by the `project` query param instead of the
* Log/trace/span listing scopes by the `project` query param instead of the
* `project:<slug>` search filter, which only matches projects that are actively
* selected in the org (see #1317). This helper resolves the slug so callers can
* pass a numeric ID.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/commands/span/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ describe("listCommand.func (project mode)", () => {
return { org: "test-org", project: "test-project" };
}
);
vi.spyOn(resolveTarget, "resolveLogProjectId").mockResolvedValue(4242);
resolveCursorSpy.mockReturnValue({
cursor: undefined,
direction: "next" as const,
Expand Down Expand Up @@ -820,6 +821,7 @@ describe("listCommand.func (project mode)", () => {
const callArgs = listSpansSpy.mock.calls[0];
const options = callArgs[2];
expect(options.allProjects).toBeUndefined();
expect(options.projectId).toBe(4242);
});

test("hint shows -c next with project target when more pages available", async () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/commands/trace/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ describe("listCommand.func", () => {
listTransactionsSpy = vi.spyOn(apiClient, "listTransactions");
findProjectsBySlugSpy = vi.spyOn(apiClient, "findProjectsBySlug");
resolveOrgAndProjectSpy = vi.spyOn(resolveTarget, "resolveOrgAndProject");
vi.spyOn(resolveTarget, "resolveLogProjectId").mockResolvedValue(4242);
resolveCursorSpy = vi.spyOn(paginationDb, "resolveCursor").mockReturnValue({
cursor: undefined,
direction: "next" as const,
Expand Down Expand Up @@ -473,6 +474,7 @@ describe("listCommand.func", () => {
limit: 50,
sort: "duration",
cursor: undefined,
projectId: 4242,
statsPeriod: "7d",
}
);
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/test/lib/api/traces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ describe("listTransactions", () => {
expect(capturedUrl).not.toMatch(/[?&]project=my-project/);
});

test("scopes via the project param when projectId is provided (#1317)", async () => {
mockOk({ data: [], meta: TX_META });

await listTransactions("my-org", "my-project", { projectId: 4242 });

expect(capturedUrl).toContain("project=4242");
expect(decodeURIComponent(capturedUrl)).not.toContain("project:my-project");
});

test("numeric project ID goes as project param", async () => {
mockOk({ data: [], meta: TX_META });

Expand Down Expand Up @@ -470,6 +479,15 @@ describe("listSpans", () => {
);
});

test("scopes via the project param when projectId is provided (#1317)", async () => {
mockOk({ data: [], meta: SPAN_META });

await listSpans("my-org", "my-project", { projectId: 4242 });

expect(capturedUrl).toContain("project=4242");
expect(decodeURIComponent(capturedUrl)).not.toContain("project:my-project");
});

test("auto-paginates when limit > 100", async () => {
const { getCapturedUrls } = mockSequential([
{
Expand Down
Loading