Skip to content
Closed
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: 1 addition & 1 deletion docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ cli/
│ │ ├── cli/ # defaults, feedback, fix, setup, upgrade
│ │ ├── dashboard/ # list, view, create, add, edit, delete
│ │ ├── event/ # view, list
│ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, merge
│ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, archive, merge
│ │ ├── log/ # list, view
│ │ ├── org/ # list, view
│ │ ├── project/ # create, delete, list, view
Expand Down
1 change: 1 addition & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ Manage Sentry issues
- `sentry issue view <issue>` — View details of a specific issue
- `sentry issue resolve <issue>` — Mark an issue as resolved
- `sentry issue unresolve <issue>` — Reopen a resolved issue
- `sentry issue archive <issue>` — Archive (ignore) an issue
- `sentry issue merge <issue...>` — Merge 2+ issues into a single canonical group

→ Full flags and examples: `references/issue.md`
Expand Down
11 changes: 11 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,17 @@ sentry issue reopen CLI-G5 # alias

Reopen a resolved issue

### `sentry issue archive <issue>`

Archive (ignore) an issue

**Flags:**
- `--duration <value> - Ignore for this many minutes`
- `--count <value> - Ignore until this many more events occur`
- `--window <value> - Time window in minutes for --count (events must occur within this window)`
- `--users <value> - Ignore until this many more users are affected`
- `--user-window <value> - Time window in minutes for --users (users must be affected within this window)`

### `sentry issue merge <issue...>`

Merge 2+ issues into a single canonical group
Expand Down
9 changes: 9 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Inject debug IDs into JavaScript files and sourcemaps

**Flags:**
- `--ext <value> - Comma-separated file extensions to process (default: .js,.cjs,.mjs)`
- `--ignore <value> - Glob pattern to exclude (gitignore-style, repeatable)`
- `--ignore-file <value> - Path to a file with gitignore-style patterns to exclude`
- `--dry-run - Show what would be modified without writing`
- `--allow-empty - Exit successfully when no JS + sourcemap pairs are found (default: error out to catch silent build misconfigurations)`

Expand All @@ -39,7 +41,14 @@ Upload sourcemaps to Sentry

**Flags:**
- `--release <value> - Release version to associate with the upload`
- `--dist <value> - Distribution identifier to disambiguate builds within a release`
- `--url-prefix <value> - URL prefix for uploaded files (default: ~/) - (default: "~/")`
- `--ext <value> - Comma-separated file extensions to process (default: .js,.cjs,.mjs)`
- `--ignore <value> - Glob pattern to exclude (gitignore-style, repeatable)`
- `--ignore-file <value> - Path to a file with gitignore-style patterns to exclude`
- `--strip-prefix <value> - Strip a prefix from uploaded file paths (e.g. 'build/')`
- `--strip-common-prefix - Automatically strip the longest common path prefix from all files`
- `--no-rewrite - Upload files as-is without injecting debug IDs`
- `--allow-empty - Exit successfully when no JS + sourcemap pairs are found (default: error out to catch silent build misconfigurations)`

**Examples:**
Expand Down
126 changes: 126 additions & 0 deletions src/commands/issue/archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* sentry issue archive (aliased: ignore)
*
* Archive (ignore) an issue, suppressing alerts until an optional
* condition is met. This maps to the "ignored" status in the Sentry API.
*/

import type { SentryContext } from "../../context.js";
import {
type IgnoreStatusDetails,
updateIssueStatus,
} from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
import { formatIssueDetails, muted } from "../../lib/formatters/index.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { logger } from "../../lib/logger.js";
import type { SentryIssue } from "../../types/index.js";
import { issueIdPositional, resolveIssue } from "./utils.js";

const log = logger.withTag("issue.archive");

const COMMAND = "archive";

type ArchiveFlags = {
readonly json: boolean;
readonly fields?: string[];
readonly duration?: number;
readonly count?: number;
readonly window?: number;
readonly users?: number;
readonly "user-window"?: number;
};

function formatArchived(issue: SentryIssue): string {
return `${muted("Archived")}\n\n${formatIssueDetails(issue)}`;
}

export const archiveCommand = buildCommand({
docs: {
brief: "Archive (ignore) an issue",
fullDescription:
"Archive an issue, suppressing alerts until an optional condition is met.\n" +
"Without any duration/count flags, the issue is archived indefinitely.\n\n" +
"Examples:\n" +
" sentry issue archive CLI-12Z\n" +
" sentry issue archive CLI-12Z --duration 60\n" +
" sentry issue archive CLI-12Z --count 100 --window 60\n" +
" sentry issue archive CLI-12Z --users 10",
},
output: {
human: formatArchived,
},
parameters: {
positional: issueIdPositional,
flags: {
duration: {
kind: "parsed",
parse: Number,
brief: "Ignore for this many minutes",
optional: true,
},
count: {
kind: "parsed",
parse: Number,
brief: "Ignore until this many more events occur",
optional: true,
},
window: {
kind: "parsed",
parse: Number,
brief:
"Time window in minutes for --count (events must occur within this window)",
optional: true,
},
users: {
kind: "parsed",
parse: Number,
brief: "Ignore until this many more users are affected",
optional: true,
},
"user-window": {
kind: "parsed",
parse: Number,
brief:
"Time window in minutes for --users (users must be affected within this window)",
optional: true,
},
},
},
async *func(this: SentryContext, flags: ArchiveFlags, issueArg: string) {
const { cwd } = this;

const { org, issue } = await resolveIssue({
issueArg,
cwd,
command: COMMAND,
});

const statusDetails: IgnoreStatusDetails = {};
if (flags.duration !== undefined) {
statusDetails.ignoreDuration = flags.duration;
}
if (flags.count !== undefined) {
statusDetails.ignoreCount = flags.count;
}
if (flags.window !== undefined) {
statusDetails.ignoreWindow = flags.window;
}
if (flags.users !== undefined) {
statusDetails.ignoreUserCount = flags.users;
}
if (flags["user-window"] !== undefined) {
statusDetails.ignoreUserWindow = flags["user-window"];
}

const hasDetails = Object.keys(statusDetails).length > 0;

const updated = await updateIssueStatus(issue.id, "ignored", {
...(hasDetails ? { statusDetails } : {}),
orgSlug: org,
});

log.debug(`Archived ${updated.shortId}`);
yield new CommandOutput<SentryIssue>(updated);
},
});
11 changes: 7 additions & 4 deletions src/commands/issue/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildRouteMap } from "../../lib/route-map.js";
import { archiveCommand } from "./archive.js";
import { eventsCommand } from "./events.js";
import { explainCommand } from "./explain.js";
import { listCommand } from "./list.js";
Expand All @@ -17,11 +18,11 @@ export const issueRoute = buildRouteMap({
view: viewCommand,
resolve: resolveCommand,
unresolve: unresolveCommand,
archive: archiveCommand,
merge: mergeCommand,
},
// `reopen` is a friendlier synonym for `unresolve` — shipped as an alias
// so either command works identically.
aliases: { reopen: "unresolve" },
// `reopen` is a friendlier synonym for `unresolve`, `ignore` for `archive`.
aliases: { reopen: "unresolve", ignore: "archive" },
defaultCommand: "view",
docs: {
brief: "Manage Sentry issues",
Expand All @@ -35,14 +36,16 @@ export const issueRoute = buildRouteMap({
" plan Generate a solution plan using Seer AI\n" +
" resolve Mark an issue as resolved (optionally in a release)\n" +
" unresolve Reopen a resolved issue (alias: reopen)\n" +
" archive Archive/ignore an issue (alias: ignore)\n" +
" merge Merge 2+ issues into a single group\n\n" +
"Magic selectors (available for view, events, explain, plan, resolve, unresolve):\n" +
"Magic selectors (available for view, events, explain, plan, resolve, unresolve, archive):\n" +
" @latest Most recent unresolved issue\n" +
" @most_frequent Issue with the highest event frequency\n\n" +
"Examples:\n" +
" sentry issue view @latest\n" +
" sentry issue events CLI-G\n" +
" sentry issue resolve CLI-12Z --in 0.26.1\n" +
" sentry issue archive CLI-AB --duration 60\n" +
" sentry issue merge CLI-K9 CLI-15H CLI-15N\n" +
" sentry issue explain @most_frequent\n" +
" sentry issue plan my-org/@latest\n\n" +
Expand Down
32 changes: 30 additions & 2 deletions src/commands/sourcemap/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CommandOutput } from "../../lib/formatters/output.js";
import {
assertDirectoryReadable,
buildEmptyDiscoveryError,
buildIgnoreMatcher,
diagnoseEmptyDiscovery,
discoverFilePairs,
type InjectResult,
Expand Down Expand Up @@ -91,6 +92,18 @@ export const injectCommand = buildCommand({
"Comma-separated file extensions to process (default: .js,.cjs,.mjs)",
optional: true,
},
ignore: {
kind: "parsed",
parse: String,
brief: "Glob pattern to exclude (gitignore-style, repeatable)",
optional: true,
},
"ignore-file": {
kind: "parsed",
parse: String,
brief: "Path to a file with gitignore-style patterns to exclude",
optional: true,
},
"dry-run": {
kind: "boolean",
brief: "Show what would be modified without writing",
Expand All @@ -109,7 +122,13 @@ export const injectCommand = buildCommand({
},
async *func(
this: SentryContext,
flags: { ext?: string; "dry-run"?: boolean; "allow-empty"?: boolean },
flags: {
ext?: string;
ignore?: string;
"ignore-file"?: string;
"dry-run"?: boolean;
"allow-empty"?: boolean;
},
dir: string
) {
// Discover pairs read-only first so we don't error after partially
Expand All @@ -123,14 +142,23 @@ export const injectCommand = buildCommand({
? new Set(extensions.map((e) => (e.startsWith(".") ? e : `.${e}`)))
: undefined;

const pairs = await discoverFilePairs(dir, extSet);
const ignorePatterns = flags.ignore
? flags.ignore.split(",").map((p) => p.trim())
: undefined;
const ignoreMatcher = await buildIgnoreMatcher(
ignorePatterns,
flags["ignore-file"]
);

const pairs = await discoverFilePairs(dir, extSet, ignoreMatcher);
if (pairs.length === 0 && !flags["allow-empty"]) {
const diag = await diagnoseEmptyDiscovery(dir, { extensions });
throw buildEmptyDiscoveryError(dir, diag);
}

const results = await injectDirectory(dir, {
extensions,
ignorePatterns,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inject command drops --ignore-file patterns during injection

High Severity

The inject command builds an ignoreMatcher from both --ignore patterns and --ignore-file, and correctly uses it for the discoverFilePairs pre-check. But it then passes only ignorePatterns (not ignoreMatcher) to injectDirectory. Inside injectDirectory, the matcher is rebuilt from just ignorePatterns, losing all patterns from --ignore-file. This makes --ignore-file silently ineffective during actual injection. The upload command correctly passes ignoreMatcher to injectDirectory; the inject command needs to do the same.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0970403. Configure here.

dryRun: flags["dry-run"],
});
Comment on lines 155 to 163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The inject command fails to pass the ignoreMatcher to injectDirectory, causing files specified in --ignore-file to be incorrectly processed during injection.
Severity: MEDIUM

Suggested Fix

Pass the fully constructed ignoreMatcher from the inject command to the injectDirectory function via the options.ignoreMatcher property, instead of only passing ignorePatterns.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/commands/sourcemap/inject.ts#L155-L163

Potential issue: The `inject` command correctly builds an `ignoreMatcher` using patterns
from both the `--ignore` flag and the `--ignore-file` flag. This matcher is used for an
initial file discovery. However, when calling the `injectDirectory` function, only the
`ignorePatterns` from the `--ignore` flag are passed. The `injectDirectory` function
then rebuilds the ignore matcher without the contents of the file specified by
`--ignore-file`. As a result, files that should be ignored via the ignore file are
incorrectly processed and have debug IDs injected.

Did we get this right? 👍 / 👎 to inform future reviews.


Expand Down
Loading
Loading