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
23 changes: 13 additions & 10 deletions src/commands/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
getReplayRequestFields,
isSupportedReplayField,
listSupportedReplayFields,
normalizeVariadicFlag,
parseReplayEnvironmentFilter,
} from "../lib/replay-search.js";
import { resolveOrgOptionalProjectFromArg } from "../lib/resolve-target.js";
Expand Down Expand Up @@ -375,10 +376,8 @@ function appendFlagHints(
if (flags.limit !== DEFAULT_LIMIT) {
parts.push(`--limit ${flags.limit}`);
}
if (flags.environment && flags.environment.length > 0) {
for (const environment of flags.environment) {
parts.push(`-e "${environment}"`);
}
for (const environment of normalizeVariadicFlag(flags.environment)) {
parts.push(`-e "${environment}"`);
}
appendPeriodHint(parts, flags.period, DEFAULT_PERIOD);
return parts.length > 0 ? `${base} ${parts.join(" ")}` : base;
Expand Down Expand Up @@ -710,11 +709,15 @@ export const exploreCommand = buildListCommand("explore", {
);

let dataset = flags.dataset;
const userSuppliedFields = flags.field && flags.field.length > 0;
let fieldList = [...defaultFieldsForDataset(dataset)];
if (userSuppliedFields) {
fieldList = flags.field;
}
// A single `-F` flag arrives from the parser as a bare string rather than a
// one-element array. Normalize once so every downstream consumer (fieldList,
// hintFlags, contextKey) sees an array — a leftover string makes the later
// `.filter`/`.join` calls throw a TypeError (CLI-28C).
const fields = normalizeVariadicFlag(flags.field);
const userSuppliedFields = fields.length > 0;
let fieldList = userSuppliedFields
? [...fields]
: [...defaultFieldsForDataset(dataset)];
const timeRange = flags.period;
const environment = parseReplayEnvironmentFilter(flags.environment);

Expand Down Expand Up @@ -806,7 +809,7 @@ export const exploreCommand = buildListCommand("explore", {
const hasMore = !!nextCursor;

const baseTarget = project ? `${org}/${project}` : `${org}/`;
const hintFlags = { ...flags, dataset };
const hintFlags = { ...flags, dataset, field: fields };
const nav = paginationHint({
hasPrev,
hasMore,
Expand Down
7 changes: 3 additions & 4 deletions src/commands/replay/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { withProgress } from "../../lib/polling.js";
import {
getReplayUserLabel,
normalizeVariadicFlag,
parseReplayEnvironmentFilter,
} from "../../lib/replay-search.js";
import { resolveOrgOptionalProjectFromArg } from "../../lib/resolve-target.js";
Expand Down Expand Up @@ -182,10 +183,8 @@ function appendReplayFlags(
const parts: string[] = [];
appendQueryHint(parts, flags.query);
appendSortHint(parts, flags.sort, DEFAULT_SORT);
if (flags.environment && flags.environment.length > 0) {
for (const environment of flags.environment) {
parts.push(`-e "${environment}"`);
}
for (const environment of normalizeVariadicFlag(flags.environment)) {
parts.push(`-e "${environment}"`);
}
appendPeriodHint(parts, flags.period, DEFAULT_PERIOD);
return parts.length > 0 ? `${base} ${parts.join(" ")}` : base;
Expand Down
29 changes: 22 additions & 7 deletions src/lib/replay-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,31 @@ export const DEFAULT_REPLAY_EXPLORE_FIELDS = [
"user.email",
] as const;

/**
* Coerce a variadic CLI flag value into an array.
*
* Stricli's variadic parser yields a bare `string` for a single occurrence of
* a flag and a `string[]` for multiple, even though the flag is declared
* variadic. Callers must normalize before spreading or iterating, otherwise a
* single value is treated as an array of characters (CLI-28C).
*/
export function normalizeVariadicFlag(
value: readonly string[] | string | undefined
): string[] {
if (value === undefined) {
return [];
}
return Array.isArray(value) ? [...value] : [value as string];
}

/** Parse repeatable and comma-separated replay environment filters. */
export function parseReplayEnvironmentFilter(
values: readonly string[] | undefined
values: readonly string[] | string | undefined
): string[] | undefined {
const parsed = values
? [...values]
.flatMap((value) => value.split(","))
.map((value) => value.trim())
.filter(Boolean)
: [];
const parsed = normalizeVariadicFlag(values)
.flatMap((value) => value.split(","))
.map((value) => value.trim())
.filter(Boolean);

return parsed.length > 0 ? parsed : undefined;
}
Expand Down
22 changes: 22 additions & 0 deletions test/commands/explore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,28 @@ describe("sentry explore", () => {
);
});

test("coerces a single -F flag (string) into an array without crashing", async () => {
// Stricli's variadic parser emits a bare string for a single `-F`, not a
// one-element array. The command must normalize it before the query and
// the pagination-hint path both use array methods on it (CLI-28C).
resolveTargetSpy.mockResolvedValue({ org: "test-org" });
const { context } = createContext();

await func.call(
context,
{
...DEFAULT_FLAGS,
field: "transaction" as unknown as string[],
},
"test-org/"
);

expect(queryEventsSpy).toHaveBeenCalledWith(
"test-org",
expect.objectContaining({ fields: ["transaction"] })
);
});

test("passes custom dataset", async () => {
resolveTargetSpy.mockResolvedValue({ org: "test-org" });
const { context } = createContext();
Expand Down
37 changes: 37 additions & 0 deletions test/lib/replay-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, expect, test } from "vitest";
import {
getReplayRequestFields,
isSupportedReplayField,
normalizeVariadicFlag,
parseReplayEnvironmentFilter,
} from "../../src/lib/replay-search.js";

describe("getReplayRequestFields", () => {
Expand Down Expand Up @@ -38,3 +40,38 @@ describe("isSupportedReplayField", () => {
expect(isSupportedReplayField("replay_type")).toBe(false);
});
});

describe("normalizeVariadicFlag", () => {
test("wraps a single string value into an array", () => {
// A single `-e` flag arrives from the parser as a bare string, not a
// one-element array (CLI-28C).
expect(normalizeVariadicFlag("production")).toEqual(["production"]);
});

test("passes an array through and returns [] for undefined", () => {
expect(normalizeVariadicFlag(["a", "b"])).toEqual(["a", "b"]);
expect(normalizeVariadicFlag(undefined)).toEqual([]);
});
});

describe("parseReplayEnvironmentFilter", () => {
test("treats a single -e value as one environment, not per-character", () => {
// Regression: a bare string must not be spread into characters.
expect(
parseReplayEnvironmentFilter("production" as unknown as string[])
).toEqual(["production"]);
});

test("splits comma-separated and repeated values", () => {
expect(parseReplayEnvironmentFilter(["prod,staging", "dev"])).toEqual([
"prod",
"staging",
"dev",
]);
});

test("returns undefined when empty", () => {
expect(parseReplayEnvironmentFilter(undefined)).toBeUndefined();
expect(parseReplayEnvironmentFilter([])).toBeUndefined();
});
});
Loading