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
2 changes: 2 additions & 0 deletions packages/cli/lint-rules/cli-oxlint-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ const BOUND_TARGET_TYPES = new Set([
"LogListArguments",
"LogViewArguments",
"ReplayArguments",
"SpanListArguments",
"SpanViewArguments",
"TraceLogsArguments",
"TraceViewArguments",
]);

Expand Down
152 changes: 99 additions & 53 deletions packages/cli/src/commands/span/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
* - **Project mode** (no trace ID): lists spans across the entire project
* - **Trace mode** (trace ID provided): lists spans within a specific trace
*
* Disambiguation uses {@link isTraceId} to detect 32-char hex trace IDs.
* Disambiguation uses {@link parseDualModeInput}: trace mode when the
* arguments name a trace ID.
*/

import type { SentryContext } from "../../context.js";
import type { SpanSortValue } from "../../lib/api/traces.js";
import { listSpans } from "../../lib/api-client.js";
import { validateLimit } from "../../lib/arg-parsing.js";
import { parseOrgProjectTarget } from "../../lib/completion-target.js";
import {
type ParsedOrgProject,
parseOrgProjectInput,
validateLimit,
} from "../../lib/arg-parsing.js";
import { ORG_PROJECT_TARGET } from "../../lib/completion-target.js";
import {
advancePaginationState,
buildPaginationContextKey,
Expand Down Expand Up @@ -42,21 +47,28 @@ import {
TARGET_PATTERN_NOTE,
} from "../../lib/list-command.js";
import { withProgress } from "../../lib/polling.js";
import {
type BoundArguments,
boundArguments,
} from "../../lib/positional-parse.js";
import {
resolveLogProjectId,
resolveProjectBoundFromArg,
resolveProjectBoundTarget,
} from "../../lib/resolve-target.js";
import { sanitizeQuery } from "../../lib/search-query.js";
import { applySentryUrlContext } from "../../lib/sentry-url-parser.js";
import {
appendPeriodHint,
serializeTimeRange,
type TimeRange,
timeRangeToApiParams,
} from "../../lib/time-range.js";
import {
type ParsedTraceTarget,
parseDualModeArgs,
applyTraceTargetInput,
parseDualModeInput,
type ResolvedTraceOrgProject,
resolveTraceOrgProject,
type TraceTargetInput,
warnIfNormalized,
} from "../../lib/trace-target.js";
import { SpanListItemSchema } from "../../types/index.js";
Expand Down Expand Up @@ -188,16 +200,66 @@ export function parseSort(value: string): SpanSortValue {
return value as SpanSortValue;
}

/** What `span list` lists, as parsed: a project's spans or a trace's. */
type SpanListInput =
| {
readonly mode: "project";
readonly target: ParsedOrgProject;
/** Server of a pasted Sentry URL, for `applySentryUrlContext` */
readonly baseUrl?: string;
}
| { readonly mode: "trace"; readonly trace: TraceTargetInput };

/** What `span list` lists, resolved. */
type SpanListTarget =
| { readonly mode: "project"; readonly org: string; readonly project: string }
| ({ readonly mode: "trace" } & ResolvedTraceOrgProject);

/** The arguments of `span list`. */
type SpanListArguments = BoundArguments<SpanListInput, SpanListTarget>;

/**
* Disambiguate span list positional arguments.
* Disambiguate span list positional arguments with
* {@link parseDualModeInput}, without side effects. Both modes need a
* project, so `<x>/<trace-id>` names project `<x>`.
*
* Thin wrapper around {@link parseDualModeArgs} that binds the
* trace-mode usage hint for span list.
* @internal Exported for testing
*/
export function parseSpanListArgs(
args: string[],
): ReturnType<typeof parseDualModeArgs> {
return parseDualModeArgs(args, TRACE_USAGE_HINT, { needsProject: true });
export function parseSpanListInput(args: readonly string[]): SpanListInput {
const input = parseDualModeInput(args, TRACE_USAGE_HINT, {
needsProject: true,
});
if (input.mode === "trace") {
return input;
}
const { parsed, baseUrl } = parseOrgProjectInput(input.target);
return { mode: "project", target: parsed, baseUrl };
}

/** Resolve `span list` arguments to the project, or the trace and project. */
async function resolveSpanListTarget(
input: SpanListInput,
context: SentryContext,
): Promise<SpanListTarget> {
if (input.mode === "trace") {
const parsed = applyTraceTargetInput(input.trace);
warnIfNormalized(parsed, "span.list");
const resolved = await resolveTraceOrgProject(
parsed,
context.cwd,
TRACE_USAGE_HINT,
);
return { mode: "trace", ...resolved };
}
if (input.baseUrl) {
applySentryUrlContext(input.baseUrl);
}
const { org, project } = await resolveProjectBoundTarget(
input.target,
context.cwd,
COMMAND_NAME,
);
return { mode: "project", org, project };
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -348,7 +410,6 @@ function jsonTransformSpanList(data: SpanListData, fields?: string[]): unknown {

/** Shared context passed to mode handlers from the Stricli command function. */
type ModeContext = {
cwd: string;
flags: ListFlags;
/** Parsed time range from the --period flag */
timeRange: TimeRange;
Expand All @@ -359,20 +420,14 @@ type ModeContext = {
/**
* Handle trace mode: list spans within a specific trace.
*
* Resolves the trace target, builds a query prefixed with `trace:{id}`,
* and returns the result with a trace-specific header and hints.
* Builds a query prefixed with `trace:{id}` and returns the result with a
* trace-specific header and hints.
*/
async function handleTraceMode(
parsed: ParsedTraceTarget,
{ traceId, org, project }: ResolvedTraceOrgProject,
ctx: ModeContext,
): Promise<{ output: SpanListData; hint?: string }> {
const { flags, cwd, extraApiFields, timeRange } = ctx;
warnIfNormalized(parsed, "span.list");
const { traceId, org, project } = await resolveTraceOrgProject(
parsed,
cwd,
TRACE_USAGE_HINT,
);
const { flags, extraApiFields, timeRange } = ctx;
const queryParts = [`trace:${traceId}`];
if (flags.query) {
queryParts.push(translateSpanQuery(flags.query));
Expand Down Expand Up @@ -449,19 +504,13 @@ async function handleTraceMode(
/**
* Handle project mode: list spans across the entire project.
*
* Resolves the org/project target and queries the spans dataset
* without a trace ID filter.
* Queries the spans dataset without a trace ID filter.
*/
async function handleProjectMode(
target: string | undefined,
{ org, project }: { readonly org: string; readonly project: string },
ctx: ModeContext,
): Promise<{ output: SpanListData; hint?: string }> {
const { flags, cwd, extraApiFields, timeRange } = ctx;
const { org, project } = await resolveProjectBoundFromArg(
target,
cwd,
COMMAND_NAME,
);
const { flags, extraApiFields, timeRange } = ctx;
// 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);
Expand Down Expand Up @@ -607,14 +656,13 @@ export const listCommand = buildListCommand("span", {
schema: SpanListItemSchema,
},
parameters: {
positional: {
kind: "array",
parameter: {
placeholder: "org/project/trace-id",
brief: "[<org>/<project>] or [<org>/<project>/]<trace-id>",
parse: parseOrgProjectTarget,
},
},
positional: boundArguments({
placeholder: "org/project/trace-id",
brief: "[<org>/<project>] or [<org>/<project>/]<trace-id>",
kind: ORG_PROJECT_TARGET,
parse: parseSpanListInput,
resolve: resolveSpanListTarget,
}),
flags: {
limit: {
kind: "parsed",
Expand Down Expand Up @@ -644,22 +692,20 @@ export const listCommand = buildListCommand("span", {
s: "sort",
},
},
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
const { cwd } = this;
async *func(
this: SentryContext,
flags: ListFlags,
target: SpanListArguments,
) {
const timeRange = flags.period;
const parsed = parseSpanListArgs(args);
const extraApiFields = extractExtraApiFields(flags.fields);
const modeCtx: ModeContext = {
cwd,
flags,
timeRange,
extraApiFields,
};
const modeCtx: ModeContext = { flags, timeRange, extraApiFields };

const resolved = await target.resolved;
const { output, hint } =
parsed.mode === "trace"
? await handleTraceMode(parsed.parsed, modeCtx)
: await handleProjectMode(parsed.target, modeCtx);
resolved.mode === "trace"
? await handleTraceMode(resolved, modeCtx)
: await handleProjectMode(resolved, modeCtx);

yield new CommandOutput(output);
return { hint };
Expand Down
79 changes: 59 additions & 20 deletions packages/cli/src/commands/trace/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { buildCommand } from "../../lib/command.js";
import { parseOrgProjectTarget } from "../../lib/completion-target.js";
import { ORG_PROJECT_TARGET } from "../../lib/completion-target.js";
import { toSearchQueryError } from "../../lib/errors.js";
import { filterFields } from "../../lib/formatters/json.js";
import { formatLogTable } from "../../lib/formatters/log.js";
Expand All @@ -25,6 +25,10 @@ import {
LIST_MAX_LIMIT,
} from "../../lib/list-command.js";
import { withProgress } from "../../lib/polling.js";
import {
type BoundArguments,
boundArguments,
} from "../../lib/positional-parse.js";
import { sanitizeQuery } from "../../lib/search-query.js";
import { buildTraceUrl } from "../../lib/sentry-web-urls.js";
import {
Expand All @@ -35,8 +39,11 @@ import {
timeRangeToApiParams,
} from "../../lib/time-range.js";
import {
parseTraceTarget,
applyTraceTargetInput,
parseTraceTargetInput,
type ResolvedTraceOrg,
resolveTraceOrg,
type TraceTargetInput,
warnIfNormalized,
} from "../../lib/trace-target.js";

Expand Down Expand Up @@ -97,6 +104,42 @@ function parseLimit(value: string): number {
return validateLimit(value, 1, LIST_MAX_LIMIT);
}

/** `trace logs` arguments, resolved. */
type TraceLogsTarget = ResolvedTraceOrg & {
/** Project the user named, to filter the trace's logs by */
readonly projectFilter?: string;
};

/** The arguments of `trace logs`. */
type TraceLogsArguments = BoundArguments<TraceTargetInput, TraceLogsTarget>;

/** Parse `trace logs` arguments, without side effects. */
function parseTraceLogsInput(args: readonly string[]): TraceTargetInput {
return parseTraceTargetInput(args, USAGE_HINT);
}

/**
* Resolve `trace logs` arguments to the trace's org; a project the user
* named becomes a filter.
*/
async function resolveTraceLogsTarget(
input: TraceTargetInput,
context: SentryContext,
): Promise<TraceLogsTarget> {
const parsed = applyTraceTargetInput(input);
warnIfNormalized(parsed, "trace.logs");
const { traceId, org } = await resolveTraceOrg(
parsed,
context.cwd,
USAGE_HINT,
);
return {
traceId,
org,
projectFilter: parsed.type === "explicit" ? parsed.project : undefined,
};
}

export const logsCommand = buildCommand({
docs: {
brief: "View logs associated with a trace",
Expand Down Expand Up @@ -151,15 +194,14 @@ export const logsCommand = buildCommand({
},
},
parameters: {
positional: {
kind: "array",
parameter: {
placeholder: "org/project/trace-id",
brief:
"[<org>/[<project>/]]<trace-id> - Optional org/project and required trace ID",
parse: parseOrgProjectTarget,
},
},
positional: boundArguments({
placeholder: "org/project/trace-id",
brief:
"[<org>/[<project>/]]<trace-id> - Optional org/project and required trace ID",
kind: ORG_PROJECT_TARGET,
parse: parseTraceLogsInput,
resolve: resolveTraceLogsTarget,
}),
flags: {
web: {
kind: "boolean",
Expand Down Expand Up @@ -202,17 +244,14 @@ export const logsCommand = buildCommand({
s: "sort",
},
},
async *func(this: SentryContext, flags: LogsFlags, ...args: string[]) {
async *func(
this: SentryContext,
flags: LogsFlags,
target: TraceLogsArguments,
) {
applyFreshFlag(flags);
const { cwd } = this;
const timeRange = flags.period;

// Parse and resolve org/trace-id (project captured for filtering)
const parsed = parseTraceTarget(args, USAGE_HINT);
warnIfNormalized(parsed, "trace.logs");
const { traceId, org } = await resolveTraceOrg(parsed, cwd, USAGE_HINT);
const projectFilter =
parsed.type === "explicit" ? parsed.project : undefined;
const { traceId, org, projectFilter } = await target.resolved;

if (flags.web) {
await openInBrowser(buildTraceUrl(org, traceId), "trace");
Expand Down
Loading
Loading