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
74 changes: 73 additions & 1 deletion packages/cli/src/commands/log/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,69 @@ function parseSingleSlashLogArg(
return null;
}

/**
* Split a fully-qualified `<org>/<project>/<log-id>` arg into its target and
* log ID, or return `null` when the arg isn't in that shape (fewer than two
* slashes, or the trailing segment isn't a 32-char hex ID).
*
* Agents often pass several fully-qualified paths to one `log view` call
* (CLI-1XC). Without this, the first path was used verbatim as the target
* (yielding project `"<project>/<log-id>"`) and the rest failed hex validation.
*/
function splitFullLogPath(
arg: string,
): { target: string; logId: string } | null {
const trimmed = arg.trim();
const firstSlash = trimmed.indexOf("/");
const lastSlash = trimmed.lastIndexOf("/");
if (firstSlash === -1 || firstSlash === lastSlash) {
return null;
}
const logId = normalizeHexId(trimmed.slice(lastSlash + 1));
if (!HEX_ID_RE.test(logId)) {
return null;
}
return { target: trimmed.slice(0, lastSlash), logId };
}

/**
* Parse multiple args where the first is a fully-qualified log path.
* Every later arg may be a fully-qualified path or a bare log ID; all paths
* must share the same org/project target.
*
* @throws {ValidationError} If the paths reference different targets
*/
function parseMultipleFullPaths(
args: string[],
target: string,
): { rawLogIds: string[]; targetArg: string } {
const rawLogIds: string[] = [];
for (const arg of args) {
const full = splitFullLogPath(arg);
if (full) {
if (full.target !== target) {
throw new ValidationError(
`Log IDs reference different projects ('${target}' and '${full.target}'). ` +
`View logs from one project per call: ${USAGE_HINT}`,
);
}
rawLogIds.push(full.logId);
} else {
rawLogIds.push(...splitNewlineArg(arg));
}
}
return { rawLogIds, targetArg: target };
}

/**
* Strip a redundant `<target>/` prefix from a log ID arg, so
* `sentry log view org/proj org/proj/<id>` resolves `<id>` (CLI-1XC).
*/
function stripTargetPrefix(rawId: string, target: string): string {
const prefix = `${target.trim()}/`;
return rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId;
}

/**
* Parse positional arguments for log view.
* Handles:
Expand Down Expand Up @@ -196,7 +259,16 @@ export function parsePositionalArgs(args: string[]): {
}
}

const rawLogIds = args.slice(1).flatMap(splitNewlineArg);
// Every arg is (or starts with) a fully-qualified `org/project/log-id`.
const firstFull = splitFullLogPath(first);
if (firstFull) {
return parseMultipleFullPaths(args, firstFull.target);
}

const rawLogIds = args
.slice(1)
.flatMap(splitNewlineArg)
.map((id) => stripTargetPrefix(id, first));
if (rawLogIds.length === 0) {
throw new ContextError("Log ID", USAGE_HINT, []);
}
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/test/commands/log/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,42 @@ describe("parsePositionalArgs", () => {
});
});

describe("multiple fully-qualified org/project/logId args (CLI-1XC)", () => {
test("splits each full path into a shared target + log IDs", () => {
const result = parsePositionalArgs([
`my-org/frontend/${ID1}`,
`my-org/frontend/${ID2}`,
]);
expect(result.targetArg).toBe("my-org/frontend");
expect(result.rawLogIds).toEqual([ID1, ID2]);
});

test("accepts a full path followed by bare log IDs", () => {
const result = parsePositionalArgs([`my-org/frontend/${ID1}`, ID2, ID3]);
expect(result.targetArg).toBe("my-org/frontend");
expect(result.rawLogIds).toEqual([ID1, ID2, ID3]);
});

test("throws ValidationError when full paths reference different projects", () => {
expect(() =>
parsePositionalArgs([
`my-org/frontend/${ID1}`,
`my-org/backend/${ID2}`,
]),
).toThrow(ValidationError);
});

test("strips a redundant target prefix from log ID args", () => {
const result = parsePositionalArgs([
"my-org/frontend",
`my-org/frontend/${ID1}`,
ID2,
]);
expect(result.targetArg).toBe("my-org/frontend");
expect(result.rawLogIds).toEqual([ID1, ID2]);
});
});

describe("the exact CLI-BC scenario", () => {
test("newline-delimited log IDs as a single arg with target", () => {
const ids = [
Expand Down
Loading