Guidelines for AI agents working in this codebase.
Sentry CLI is a command-line interface for Sentry, built with Bun and Stricli.
- Zero-config experience - Auto-detect project context from DSNs in source code and env files
- AI-powered debugging - Integrate Seer AI for root cause analysis and fix plans
- Developer-friendly - Follow
ghCLI conventions for intuitive UX - Agent-friendly - JSON output and predictable behavior for AI coding agents
- Fast - Native binaries via Bun, SQLite caching for API responses
- DSN Auto-Detection - Scans
.envfiles and source code (JS, Python, Go, Java, Ruby, PHP) to find Sentry DSNs - Project Root Detection - Walks up from CWD to find project boundaries using VCS, language, and build markers
- Directory Name Inference - Fallback project matching using bidirectional word boundary matching
- Multi-Region Support - Automatic region detection with fan-out to regional APIs (us.sentry.io, de.sentry.io)
- Monorepo Support - Generates short aliases for multiple projects
- Seer AI Integration -
issue explainandissue plancommands for AI analysis - OAuth Device Flow - Secure authentication without browser redirects
Before working on this codebase, read the Cursor rules:
.cursor/rules/bun-cli.mdc- Bun API usage, file I/O, process spawning, testing.cursor/rules/ultracite.mdc- Code style, formatting, linting rules
Note: Always check
package.jsonfor the latest scripts.
# Development
bun install # Install dependencies
bun run dev # Run CLI in dev mode
bun run --env-file=.env.local src/bin.ts # Dev with env vars
# Build
bun run build # Build for current platform
bun run build:all # Build for all platforms
# Type Checking
bun run typecheck # Check types
# Linting & Formatting
bun run lint # Check for issues
bun run lint:fix # Auto-fix issues (run before committing)
# Testing
bun test # Run all tests
bun test path/to/file.test.ts # Run single test file
bun test --watch # Watch mode
bun test --filter "test name" # Run tests matching pattern
bun run test:unit # Run unit tests only
bun run test:e2e # Run e2e tests onlyCRITICAL: All packages must be in devDependencies, never dependencies. Everything is bundled at build time via esbuild. CI enforces this with bun run check:deps.
When adding a package, always use bun add -d <package> (the -d flag).
When the @sentry/api SDK provides types for an API response, import them directly from @sentry/api instead of creating redundant Zod schemas in src/types/sentry.ts.
CRITICAL: This project uses Bun as runtime. Always prefer Bun-native APIs over Node.js equivalents.
Read the full guidelines in .cursor/rules/bun-cli.mdc.
Bun Documentation: https://bun.sh/docs - Consult these docs when unsure about Bun APIs.
| Task | Use This | NOT This |
|---|---|---|
| Read file | await Bun.file(path).text() |
fs.readFileSync() |
| Write file | await Bun.write(path, content) |
fs.writeFileSync() |
| Check file exists | await Bun.file(path).exists() |
fs.existsSync() |
| Spawn process | Bun.spawn() |
child_process.spawn() |
| Shell commands | Bun.$\command`` |
child_process.exec() |
| Find executable | Bun.which("git") |
which package |
| Glob patterns | new Bun.Glob() |
glob / fast-glob packages |
| Sleep | await Bun.sleep(ms) |
setTimeout with Promise |
| Parse JSON file | await Bun.file(path).json() |
Read + JSON.parse |
Exception: Use node:fs for directory creation with permissions:
import { mkdirSync } from "node:fs";
mkdirSync(dir, { recursive: true, mode: 0o700 });Exception: Bun.$ (shell tagged template) has no shim in script/node-polyfills.ts and will crash on the npm/node distribution. Until a shim is added, use execSync from node:child_process for shell commands that must work in both runtimes:
import { execSync } from "node:child_process";
const result = execSync("id -u username", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });cli/
├── src/
│ ├── bin.ts # Entry point
│ ├── app.ts # Stricli application setup
│ ├── context.ts # Dependency injection context
│ ├── commands/ # CLI commands
│ │ ├── auth/ # login, logout, status, refresh
│ │ ├── event/ # view
│ │ ├── issue/ # list, view, explain, plan
│ │ ├── org/ # list, view
│ │ ├── project/ # list, view
│ │ ├── span/ # list, view
│ │ ├── trace/ # list, view, logs
│ │ ├── log/ # list, view
│ │ ├── trial/ # list, start
│ │ ├── cli/ # fix, upgrade, feedback, setup
│ │ ├── api.ts # Direct API access command
│ │ └── help.ts # Help command
│ ├── lib/ # Shared utilities
│ │ ├── command.ts # buildCommand wrapper (telemetry + output)
│ │ ├── api-client.ts # Barrel re-export for API modules
│ │ ├── api/ # Domain API modules
│ │ │ ├── infrastructure.ts # Shared helpers, types, raw requests
│ │ │ ├── organizations.ts
│ │ │ ├── projects.ts
│ │ │ ├── issues.ts
│ │ │ ├── events.ts
│ │ │ ├── traces.ts # Trace + span listing
│ │ │ ├── logs.ts
│ │ │ ├── seer.ts
│ │ │ └── trials.ts
│ │ ├── region.ts # Multi-region resolution
│ │ ├── telemetry.ts # Sentry SDK instrumentation
│ │ ├── sentry-urls.ts # URL builders for Sentry
│ │ ├── hex-id.ts # Hex ID validation (32-char + 16-char span)
│ │ ├── trace-id.ts # Trace ID validation wrapper
│ │ ├── db/ # SQLite database layer
│ │ │ ├── instance.ts # Database singleton
│ │ │ ├── schema.ts # Table definitions
│ │ │ ├── migration.ts # Schema migrations
│ │ │ ├── utils.ts # SQL helpers (upsert)
│ │ │ ├── auth.ts # Token storage
│ │ │ ├── user.ts # User info cache
│ │ │ ├── regions.ts # Org→region URL cache
│ │ │ ├── defaults.ts # Default org/project
│ │ │ ├── pagination.ts # Cursor pagination storage
│ │ │ ├── dsn-cache.ts # DSN resolution cache
│ │ │ ├── project-cache.ts # Project data cache
│ │ │ ├── project-root-cache.ts # Project root cache
│ │ │ ├── project-aliases.ts # Monorepo alias mappings
│ │ │ └── version-check.ts # Version check cache
│ │ ├── dsn/ # DSN detection system
│ │ │ ├── detector.ts # High-level detection API
│ │ │ ├── scanner.ts # File scanning logic
│ │ │ ├── code-scanner.ts # Code file DSN extraction
│ │ │ ├── project-root.ts # Project root detection
│ │ │ ├── parser.ts # DSN parsing utilities
│ │ │ ├── resolver.ts # DSN to org/project resolution
│ │ │ ├── fs-utils.ts # File system helpers
│ │ │ ├── env.ts # Environment variable detection
│ │ │ ├── env-file.ts # .env file parsing
│ │ │ ├── errors.ts # DSN-specific errors
│ │ │ ├── types.ts # Type definitions
│ │ │ └── languages/ # Per-language DSN extractors
│ │ │ ├── javascript.ts
│ │ │ ├── python.ts
│ │ │ ├── go.ts
│ │ │ ├── java.ts
│ │ │ ├── ruby.ts
│ │ │ └── php.ts
│ │ ├── formatters/ # Output formatting
│ │ │ ├── human.ts # Human-readable output
│ │ │ ├── json.ts # JSON output
│ │ │ ├── output.ts # Output utilities
│ │ │ ├── seer.ts # Seer AI response formatting
│ │ │ ├── colors.ts # Terminal colors
│ │ │ ├── markdown.ts # Markdown → ANSI renderer
│ │ │ ├── trace.ts # Trace/span formatters
│ │ │ ├── time-utils.ts # Shared time/duration utils
│ │ │ ├── table.ts # Table rendering
│ │ │ └── log.ts # Log entry formatting
│ │ ├── oauth.ts # OAuth device flow
│ │ ├── errors.ts # Error classes
│ │ ├── resolve-target.ts # Org/project resolution
│ │ ├── resolve-issue.ts # Issue ID resolution
│ │ ├── issue-id.ts # Issue ID parsing utilities
│ │ ├── arg-parsing.ts # Argument parsing helpers
│ │ ├── alias.ts # Alias generation
│ │ ├── promises.ts # Promise utilities
│ │ ├── polling.ts # Polling utilities
│ │ ├── upgrade.ts # CLI upgrade functionality
│ │ ├── version-check.ts # Version checking
│ │ ├── browser.ts # Open URLs in browser
│ │ ├── clipboard.ts # Clipboard access
│ │ └── qrcode.ts # QR code generation
│ └── types/ # TypeScript types and Zod schemas
│ ├── sentry.ts # Sentry API types
│ ├── config.ts # Configuration types
│ ├── oauth.ts # OAuth types
│ └── seer.ts # Seer AI types
├── test/ # Test files (mirrors src/ structure)
│ ├── lib/ # Unit tests for lib/
│ │ ├── *.test.ts # Standard unit tests
│ │ ├── *.property.test.ts # Property-based tests
│ │ └── db/
│ │ ├── *.test.ts # DB unit tests
│ │ └── *.model-based.test.ts # Model-based tests
│ ├── model-based/ # Model-based testing helpers
│ │ └── helpers.ts # Isolated DB context, constants
│ ├── commands/ # Unit tests for commands/
│ ├── e2e/ # End-to-end tests
│ ├── fixtures/ # Test fixtures
│ └── mocks/ # Test mocks
├── docs/ # Documentation site (Astro + Starlight)
├── script/ # Build and utility scripts
├── .cursor/rules/ # Cursor AI rules (read these!)
└── biome.jsonc # Linting config (extends ultracite)
Commands use Stricli wrapped by src/lib/command.ts.
CRITICAL: Import buildCommand from ../../lib/command.js, NEVER from @stricli/core directly — the wrapper adds telemetry, --json/--fields injection, and output rendering.
Pattern:
import { buildCommand } from "../../lib/command.js";
import type { SentryContext } from "../../context.js";
import { CommandOutput } from "../../lib/formatters/output.js";
export const myCommand = buildCommand({
docs: {
brief: "Short description",
fullDescription: "Detailed description",
},
output: {
human: formatMyData, // (data: T) => string
jsonTransform: jsonTransformMyData, // optional: (data: T, fields?) => unknown
jsonExclude: ["humanOnlyField"], // optional: strip keys from JSON
},
parameters: {
flags: {
limit: { kind: "parsed", parse: Number, brief: "Max items", default: 10 },
},
},
async *func(this: SentryContext, flags) {
const data = await fetchData();
yield new CommandOutput(data);
return { hint: "Tip: use --json for machine-readable output" };
},
});Key rules:
- Functions are
async *func()generators — yieldnew CommandOutput(data), return{ hint }. output.humanreceives the same data object that gets serialized to JSON — no divergent-data paths.- The wrapper auto-injects
--jsonand--fieldsflags. Do NOT add your ownjsonflag. - Do NOT use
stdout.write()orif (flags.json)branching — the wrapper handles it.
Route groups use Stricli's buildRouteMap wrapped by src/lib/route-map.ts.
CRITICAL: Import buildRouteMap from ../../lib/route-map.js, NEVER from @stricli/core directly — the wrapper auto-injects standard subcommand aliases based on which route keys exist:
| Route | Auto-aliases |
|---|---|
list |
ls |
view |
show |
delete |
remove, rm |
create |
new |
Manually specified aliases in aliases are merged with (and take precedence over) auto-generated ones. Do NOT manually add aliases that are already in the standard set above.
import { buildRouteMap } from "../../lib/route-map.js";
export const myRoute = buildRouteMap({
routes: {
list: listCommand,
view: viewCommand,
create: createCommand,
},
defaultCommand: "view",
// No need for aliases — ls, show, and new are auto-injected.
// Only add aliases for non-standard mappings:
// aliases: { custom: "list" },
docs: {
brief: "Manage my resources",
},
});Use parseSlashSeparatedArg from src/lib/arg-parsing.ts for the standard [<org>/<project>/]<id> pattern. Required identifiers (trace IDs, span IDs) should be positional args, not flags.
import { parseSlashSeparatedArg, parseOrgProjectArg } from "../../lib/arg-parsing.js";
// "my-org/my-project/abc123" → { id: "abc123", targetArg: "my-org/my-project" }
const { id, targetArg } = parseSlashSeparatedArg(first, "Trace ID", USAGE_HINT);
const parsed = parseOrgProjectArg(targetArg);
// parsed.type: "auto-detect" | "explicit" | "project-search" | "org-all"Reference: span/list.ts, trace/view.ts, event/view.ts
All non-trivial human output must use the markdown rendering pipeline:
- Build markdown strings with helpers:
mdKvTable(),colorTag(),escapeMarkdownCell(),renderMarkdown() - NEVER use raw
muted()/ chalk in output strings — usecolorTag("muted", text)inside markdown - Tree-structured output (box-drawing characters) that can't go through
renderMarkdown()should use theplainSafeMutedpattern:isPlainOutput() ? text : muted(text) isPlainOutput()precedence:SENTRY_PLAIN_OUTPUT>NO_COLOR>FORCE_COLOR(TTY only) >!isTTYisPlainOutput()lives insrc/lib/formatters/plain-detect.ts(re-exported frommarkdown.tsfor compat)
Reference: formatters/trace.ts (formatAncestorChain), formatters/human.ts (plainSafeMuted)
Mutation (create/delete) commands use shared infrastructure from src/lib/mutate-command.ts,
paralleling list-command.ts for list commands.
Delete commands MUST use buildDeleteCommand() instead of buildCommand(). It:
- Auto-injects
--yes,--force,--dry-runflags with-y,-f,-naliases - Runs a non-interactive safety guard before
func()— refuses to proceed if stdin is not a TTY and--yes/--forcewas not passed (dry-run bypasses) - Options to skip specific injections (
noForceFlag,noDryRunFlag,noNonInteractiveGuard)
import { buildDeleteCommand, confirmByTyping, isConfirmationBypassed, requireExplicitTarget } from "../../lib/mutate-command.js";
export const deleteCommand = buildDeleteCommand({
// Same args as buildCommand — flags/aliases auto-injected
async *func(this: SentryContext, flags, target) {
requireExplicitTarget(parsed, "Entity", "sentry entity delete <target>");
if (flags["dry-run"]) { yield preview; return; }
if (!isConfirmationBypassed(flags)) {
if (!await confirmByTyping(expected, promptMessage)) return;
}
await doDelete();
},
});Create commands import DRY_RUN_FLAG and DRY_RUN_ALIASES for consistent dry-run support:
import { DRY_RUN_FLAG, DRY_RUN_ALIASES } from "../../lib/mutate-command.js";
// In parameters:
flags: { "dry-run": DRY_RUN_FLAG, team: { ... } },
aliases: { ...DRY_RUN_ALIASES, t: "team" },Key utilities in mutate-command.ts:
isConfirmationBypassed(flags)— true if--yesor--forceis setguardNonInteractive(flags)— throws in non-interactive mode without--yesconfirmByTyping(expected, message)— type-out confirmation promptrequireExplicitTarget(parsed, entityType, usage)— blocks auto-detect for safetyDESTRUCTIVE_FLAGS/DESTRUCTIVE_ALIASES— spreadable bundles for manual use
All list commands with API pagination MUST use the shared cursor-stack
infrastructure for bidirectional pagination (-c next / -c prev):
import { LIST_CURSOR_FLAG } from "../../lib/list-command.js";
import {
buildPaginationContextKey, resolveCursor,
advancePaginationState, hasPreviousPage,
} from "../../lib/db/pagination.js";
export const PAGINATION_KEY = "my-entity-list";
// In buildCommand:
flags: { cursor: LIST_CURSOR_FLAG },
aliases: { c: "cursor" },
// In func():
const contextKey = buildPaginationContextKey("entity", `${org}/${project}`, {
sort: flags.sort, q: flags.query,
});
const { cursor, direction } = resolveCursor(flags.cursor, PAGINATION_KEY, contextKey);
const { data, nextCursor } = await listEntities(org, project, { cursor, ... });
advancePaginationState(PAGINATION_KEY, contextKey, direction, nextCursor);
const hasPrev = hasPreviousPage(PAGINATION_KEY, contextKey);
const hasMore = !!nextCursor;Cursor stack model: The DB stores a JSON array of page-start cursors
plus a page index. Each entry is an opaque string — plain API cursors,
compound cursors (issue list), or extended cursors with mid-page bookmarks
(dashboard list). -c next increments the index, -c prev decrements it,
-c first resets to 0. The stack truncates on back-then-forward to avoid
stale entries. "last" is a silent alias for "next".
Hint rules: Show -c prev when hasPreviousPage() returns true.
Show -c next when hasMore is true. Include both nextCursor and
hasPrev in the JSON envelope.
Navigation hint generation: Use paginationHint() from
src/lib/list-command.ts to build bidirectional navigation strings.
Pass it pre-built prevHint/nextHint command strings and it returns
the combined "Prev: X | Next: Y" string (or single-direction, or "").
Do NOT assemble navParts arrays manually — the shared helper ensures
consistent formatting across all list commands.
import { paginationHint } from "../../lib/list-command.js";
const nav = paginationHint({
hasPrev,
hasMore,
prevHint: `sentry entity list ${org}/ -c prev`,
nextHint: `sentry entity list ${org}/ -c next`,
});
if (items.length === 0 && nav) {
hint = `No entities on this page. ${nav}`;
} else if (hasMore) {
header = `Showing ${items.length} entities (more available)\n${nav}`;
} else if (nav) {
header = `Showing ${items.length} entities\n${nav}`;
}Three abstraction levels for list commands (prefer the highest level that fits your use case):
-
buildOrgListCommand(team/repo list) — Fully automatic. Pagination hints, cursor management, JSON envelope, and human formatting are all handled internally. New simple org-scoped list commands should use this. -
dispatchOrgScopedListwith overrides (project/issue list) — Automatic for most modes; custom"org-all"override callsresolveCursor+advancePaginationState+paginationHintmanually. -
buildListCommandwith manual pagination (trace/span/dashboard list) — Command manages its own pagination loop. Must callresolveCursor,advancePaginationState,hasPreviousPage, andpaginationHintdirectly.
Auto-pagination for large limits:
When --limit exceeds API_MAX_PER_PAGE (100), list commands MUST transparently
fetch multiple pages to fill the requested limit. Cap perPage at
Math.min(flags.limit, API_MAX_PER_PAGE) and loop until results.length >= limit
or pages are exhausted. This matches the listIssuesAllPages pattern.
const perPage = Math.min(flags.limit, API_MAX_PER_PAGE);
for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
const { data, nextCursor } = await listPaginated(org, { perPage, cursor });
results.push(...data);
if (results.length >= flags.limit || !nextCursor) break;
cursor = nextCursor;
}Never pass a per_page value larger than API_MAX_PER_PAGE to the API — the
server silently caps it, causing the command to return fewer items than requested.
Reference template: trace/list.ts, span/list.ts, dashboard/list.ts
Use shared validators from src/lib/hex-id.ts:
validateHexId(value, label)— 32-char hex IDs (trace IDs, log IDs). Auto-strips UUID dashes.validateSpanId(value)— 16-char hex span IDs. Auto-strips dashes.validateTraceId(value)— thin wrapper aroundvalidateHexIdinsrc/lib/trace-id.ts.
All normalize to lowercase. Throw ValidationError on invalid input.
Use "date" for timestamp-based sort (not "time"). Export sort types from the API layer (e.g., SpanSortValue from api/traces.ts), import in commands. This matches issue list, trace list, and span list.
All command docs and skill files are generated via bun run generate:docs (which runs generate:command-docs then generate:skill). This runs automatically as part of dev, build, typecheck, and test scripts.
- Command docs (
docs/src/content/docs/commands/*.md) are gitignored and generated from CLI metadata + hand-written fragments indocs/src/fragments/commands/. - Skill files (
plugins/sentry-cli/skills/sentry-cli/) are committed (consumed by external plugin systems) and auto-committed by CI when stale. - Edit fragments in
docs/src/fragments/commands/for custom examples and guides. bun run check:fragmentsvalidates fragment ↔ route consistency.- Positional
placeholdervalues must be descriptive:"org/project/trace-id"not"args".
All config and API types use Zod schemas:
import { z } from "zod";
export const MySchema = z.object({
field: z.string(),
optional: z.number().optional(),
});
export type MyType = z.infer<typeof MySchema>;
// Validate data
const result = MySchema.safeParse(data);
if (result.success) {
// result.data is typed
}- Define Zod schemas alongside types in
src/types/*.ts - Key type files:
sentry.ts(API types),config.ts(configuration),oauth.ts(auth flow),seer.ts(Seer AI) - Re-export from
src/types/index.ts - Use
typeimports:import type { MyType } from "../types/index.js"
Use the upsert() helper from src/lib/db/utils.ts to reduce SQL boilerplate:
import { upsert, runUpsert } from "../db/utils.js";
// Generate UPSERT statement
const { sql, values } = upsert("table", { id: 1, name: "foo" }, ["id"]);
db.query(sql).run(...values);
// Or use convenience wrapper
runUpsert(db, "table", { id: 1, name: "foo" }, ["id"]);
// Exclude columns from update
const { sql, values } = upsert(
"users",
{ id: 1, name: "Bob", created_at: now },
["id"],
{ excludeFromUpdate: ["created_at"] }
);All CLI errors extend the CliError base class from src/lib/errors.ts:
// Error hierarchy in src/lib/errors.ts
CliError (base)
├── ApiError (HTTP/API failures - status, detail, endpoint)
├── AuthError (authentication - reason: 'not_authenticated' | 'expired' | 'invalid')
├── ConfigError (configuration - suggestion?)
├── ContextError (missing context - resource, command, alternatives)
├── ResolutionError (value provided but not found - resource, headline, hint, suggestions)
├── ValidationError (input validation - field?)
├── DeviceFlowError (OAuth flow - code)
├── SeerError (Seer AI - reason: 'not_enabled' | 'no_budget' | 'ai_disabled')
└── UpgradeError (upgrade - reason: 'unknown_method' | 'network_error' | 'execution_failed' | 'version_not_found')Choosing between ContextError, ResolutionError, and ValidationError:
| Scenario | Error Class | Example |
|---|---|---|
| User omitted a required value | ContextError |
No org/project provided |
| User provided a value that wasn't found | ResolutionError |
Project 'cli' not found |
| User input is malformed | ValidationError |
Invalid hex ID format |
ContextError rules:
commandmust be a single-line CLI usage example (e.g.,"sentry org view <slug>")- Constructor throws if
commandcontains\n(catches misuse in tests) - Pass
alternatives: []when defaults are irrelevant (e.g., for missing Trace ID, Event ID) - Use
" and "inresourcefor plural grammar:"Trace ID and span ID"→ "are required"
CI enforcement: bun run check:errors scans for ContextError with multiline commands and CliError with ad-hoc "Try:" strings.
// Usage examples
throw new ContextError("Organization", "sentry org view <org-slug>");
throw new ContextError("Trace ID", "sentry trace view <trace-id>", []); // no alternatives
throw new ResolutionError("Project 'cli'", "not found", "sentry issue list <org>/cli", [
"No project with this slug found in any accessible organization",
]);
throw new ValidationError("Invalid trace ID format", "traceId");Fuzzy suggestions in resolution errors:
When a user-provided name/title doesn't match any entity, use fuzzyMatch() from
src/lib/fuzzy.ts to suggest similar candidates instead of listing all entities
(which can be overwhelming). Show at most 5 fuzzy matches.
Reference: resolveDashboardId() in src/commands/dashboard/resolve.ts.
When a user provides the wrong type of identifier (e.g., an issue short ID where a trace ID is expected), commands should auto-recover when the user's intent is unambiguous:
- Detect the actual entity type using helpers like
looksLikeIssueShortId(),SPAN_ID_RE,HEX_ID_RE, or non-hex character checks. - Resolve the input to the correct type (e.g., issue → latest event → trace ID).
- Warn via
log.warn()explaining what happened. - Show the result with a return
hintnudging toward the correct command.
When recovery is ambiguous or impossible, keep the existing error but add entity-aware suggestions (e.g., "This looks like a span ID").
Detection helpers:
looksLikeIssueShortId(value)— uppercase dash-separated (e.g.,CLI-G5)SPAN_ID_RE.test(value)— 16-char hex (span ID)HEX_ID_RE.test(value)— 32-char hex (trace/event/log ID)/[^0-9a-f]/.test(normalized)— non-hex characters → likely a slug/name
Reference implementations:
event/view.ts— issue short ID → latest event redirectspan/view.ts—traceId/spanIdslash format → auto-splittrace/view.ts— issue short ID → issue's trace redirecthex-id.ts— entity-aware error hints invalidateHexId/validateSpanId
All config operations are async. Always await:
const token = await getAuthToken();
const isAuth = await isAuthenticated();
await setAuthToken(token, expiresIn);- Use
.jsextension for local imports (ESM requirement) - Group: external packages first, then local imports
- Use
typekeyword for type-only imports
import { z } from "zod";
import { buildCommand } from "../../lib/command.js";
import type { SentryContext } from "../../context.js";
import { getAuthToken } from "../../lib/config.js";Two abstraction levels exist for list commands:
-
src/lib/list-command.ts—buildOrgListCommandfactory + shared Stricli parameter constants (LIST_TARGET_POSITIONAL,LIST_JSON_FLAG,LIST_CURSOR_FLAG,buildListLimitFlag). Use this for simple entity lists liketeam listandrepo list. -
src/lib/org-list.ts—dispatchOrgScopedListwithOrgListConfigand a 4-mode handler map:auto-detect,explicit,org-all,project-search. Complex commands (project list,issue list) calldispatchOrgScopedListwith anoverridesmap directly instead of usingbuildOrgListCommand.
Key rules when writing overrides:
- Each mode handler receives a
HandlerContext<T>with the narrowedparsedplus shared I/O (stdout,cwd,flags). Access parsed fields viactx.parsed.org,ctx.parsed.projectSlug, etc. — no manualExtract<>casts needed. - Commands with extra fields (e.g.,
stderr,setContext) spread the context and add them:(ctx) => handle({ ...ctx, flags, stderr, setContext }). Overridectx.flagswith the command-specific flags type when needed. resolveCursor()must be called inside theorg-alloverride closure, not beforedispatchOrgScopedList, so that--cursorvalidation errors fire correctly for non-org-all modes.handleProjectSearcherrors must use"Project"as theContextErrorresource, notconfig.entityName.- Always set
orgSlugMatchBehaviorondispatchOrgScopedListto declare how bare-slug org matches are handled. Use"redirect"for commands where listing all entities in the org makes sense (e.g.,project list,team list,issue list). Use"error"for commands where org-all redirect is inappropriate. The pre-check uses cached orgs to avoid N API calls — when the cache is cold, the handler's own org-slug check serves as a safety net (throwsResolutionErrorwith a hint).
- Standalone list commands (e.g.,
span list,trace list) that don't use org-scoped dispatch wire pagination directly infunc(). See the "List Command Pagination" section above for the pattern.
- Prefer JSDoc over inline comments.
- Code should be readable without narrating what it already says.
Add JSDoc comments on:
- Every exported function, class, and type (and important internal ones).
- Types/interfaces: document each field/property (what it represents, units, allowed values, meaning of
null, defaults).
Include in JSDoc:
- What it does
- Key business rules / constraints
- Assumptions and edge cases
- Side effects
- Why it exists (when non-obvious)
Inline comments are allowed only when they add information the code cannot express:
- "Why" - business reason, constraint, historical context
- Non-obvious behavior - surprising edge cases
- Workarounds - bugs in dependencies, platform quirks
- Hardcoded values - why hardcoded, what would break if changed
Inline comments are NOT allowed if they just restate the code:
// Bad:
if (!person) // if no person
i++ // increment i
return result // return result
// Good:
// Required by GDPR Article 17 - user requested deletion
await deleteUserData(userId)- ASCII art section dividers - Do not use decorative box-drawing characters like
─────────to create section headers. Use standard JSDoc comments or simple// Section Namecomments instead.
Minimal comments, maximum clarity. Comments explain intent and reasoning, not syntax.
Prefer property-based and model-based testing over traditional unit tests. These approaches find edge cases automatically and provide better coverage with less code.
fast-check Documentation: https://fast-check.dev/docs/core-blocks/arbitraries/
- Model-Based Tests - For stateful systems (database, caches, state machines)
- Property-Based Tests - For pure functions, parsing, validation, transformations
- Unit Tests - Only for trivial cases or when properties are hard to express
| Type | Pattern | Location |
|---|---|---|
| Property-based | *.property.test.ts |
test/lib/ |
| Model-based | *.model-based.test.ts |
test/lib/db/ |
| Unit tests | *.test.ts |
test/ (mirrors src/) |
| E2E tests | *.test.ts |
test/e2e/ |
Tests that need a database or config directory must use useTestConfigDir() from test/helpers.ts. This helper:
- Creates a unique temp directory in
beforeEach - Sets
SENTRY_CONFIG_DIRto point at it - Restores (never deletes) the env var in
afterEach - Closes the database and cleans up temp files
NEVER do any of these in test files:
delete process.env.SENTRY_CONFIG_DIR— This pollutes other test files that load after yoursconst baseDir = process.env[CONFIG_DIR_ENV_VAR]!at module scope — This captures a value that may be stale- Manual
beforeEach/afterEachthat sets/deletesSENTRY_CONFIG_DIR
Why: Bun runs test files sequentially in one thread (load → run all tests → load next file). If your afterEach deletes the env var, the next file's module-level code reads undefined, causing TypeError: The "paths[0]" property must be of type string.
// CORRECT: Use the helper
import { useTestConfigDir } from "../helpers.js";
const getConfigDir = useTestConfigDir("my-test-prefix-");
// If you need the directory path in a test:
test("example", () => {
const dir = getConfigDir();
});
// WRONG: Manual env var management
beforeEach(() => { process.env.SENTRY_CONFIG_DIR = tmpDir; });
afterEach(() => { delete process.env.SENTRY_CONFIG_DIR; }); // BUG!Use property-based tests when verifying invariants that should hold for any valid input.
import { describe, expect, test } from "bun:test";
import { constantFrom, assert as fcAssert, property, tuple } from "fast-check";
import { DEFAULT_NUM_RUNS } from "../model-based/helpers.js";
// Define arbitraries (random data generators)
const slugArb = array(constantFrom(..."abcdefghijklmnopqrstuvwxyz0123456789".split("")), {
minLength: 1,
maxLength: 15,
}).map((chars) => chars.join(""));
describe("property: myFunction", () => {
test("is symmetric", () => {
fcAssert(
property(slugArb, slugArb, (a, b) => {
// Properties should always hold regardless of input
expect(myFunction(a, b)).toBe(myFunction(b, a));
}),
{ numRuns: DEFAULT_NUM_RUNS }
);
});
test("round-trip: encode then decode returns original", () => {
fcAssert(
property(validInputArb, (input) => {
const encoded = encode(input);
const decoded = decode(encoded);
expect(decoded).toEqual(input);
}),
{ numRuns: DEFAULT_NUM_RUNS }
);
});
});Good candidates for property-based testing:
- Parsing functions (DSN, issue IDs, aliases)
- Encoding/decoding (round-trip invariant)
- Symmetric operations (a op b = b op a)
- Idempotent operations (f(f(x)) = f(x))
- Validation functions (valid inputs accepted, invalid rejected)
See examples: test/lib/dsn.property.test.ts, test/lib/alias.property.test.ts, test/lib/issue-id.property.test.ts
Use model-based tests for stateful systems where sequences of operations should maintain invariants.
import { describe, expect, test } from "bun:test";
import {
type AsyncCommand,
asyncModelRun,
asyncProperty,
commands,
assert as fcAssert,
} from "fast-check";
import { createIsolatedDbContext, DEFAULT_NUM_RUNS } from "../../model-based/helpers.js";
// Define a simplified model of expected state
type DbModel = {
entries: Map<string, string>;
};
// Define commands that operate on both model and real system
class SetCommand implements AsyncCommand<DbModel, RealDb> {
constructor(readonly key: string, readonly value: string) {}
check = () => true;
async run(model: DbModel, real: RealDb): Promise<void> {
// Apply to real system
await realSet(this.key, this.value);
// Update model
model.entries.set(this.key, this.value);
}
toString = () => `set("${this.key}", "${this.value}")`;
}
class GetCommand implements AsyncCommand<DbModel, RealDb> {
constructor(readonly key: string) {}
check = () => true;
async run(model: DbModel, real: RealDb): Promise<void> {
const realValue = await realGet(this.key);
const expectedValue = model.entries.get(this.key);
// Verify real system matches model
expect(realValue).toBe(expectedValue);
}
toString = () => `get("${this.key}")`;
}
describe("model-based: database", () => {
test("random sequences maintain consistency", () => {
fcAssert(
asyncProperty(commands(allCommandArbs), async (cmds) => {
const cleanup = createIsolatedDbContext();
try {
await asyncModelRun(
() => ({ model: { entries: new Map() }, real: {} }),
cmds
);
} finally {
cleanup();
}
}),
{ numRuns: DEFAULT_NUM_RUNS }
);
});
});Good candidates for model-based testing:
- Database operations (auth, caches, regions)
- Stateful caches with invalidation
- Systems with cross-cutting invariants (e.g., clearAuth also clears regions)
See examples: test/lib/db/model-based.test.ts, test/lib/db/dsn-cache.model-based.test.ts
Use test/model-based/helpers.ts for shared utilities:
import { createIsolatedDbContext, DEFAULT_NUM_RUNS } from "../model-based/helpers.js";
// Create isolated DB for each test run (prevents interference)
const cleanup = createIsolatedDbContext();
try {
// ... test code
} finally {
cleanup();
}
// Use consistent number of runs across tests
fcAssert(property(...), { numRuns: DEFAULT_NUM_RUNS }); // 50 runsUse traditional unit tests only when:
- Testing trivial logic with obvious expected values
- Properties are difficult to express or would be tautological
- Testing error messages or specific output formatting
- Integration with external systems (E2E tests)
When a *.property.test.ts file exists for a module, do not add unit tests that re-check the same invariants with hardcoded examples. Before adding a unit test, check whether the companion property file already generates random inputs for that invariant.
Unit tests that belong alongside property tests:
- Edge cases outside the property generator's range (e.g., self-hosted DSNs when the arbitrary only produces SaaS ones)
- Specific output format documentation (exact strings, column layouts, rendered vs plain mode)
- Concurrency/timing behavior that property tests cannot express
- Integration tests exercising multiple functions together (e.g.,
writeJsonListenvelope shape)
Unit tests to avoid when property tests exist:
- "returns true for valid input" / "returns false for invalid input" — the property test already covers this with random inputs
- Basic round-trip assertions — property tests check
decode(encode(x)) === xfor allx - Hardcoded examples of invariants like idempotency, symmetry, or subset relationships
When adding property tests for a function that already has unit tests, remove the unit tests that become redundant. Add a header comment to the unit test file noting which invariants live in the property file:
/**
* Note: Core invariants (round-trips, validation, ordering) are tested via
* property-based tests in foo.property.test.ts. These tests focus on edge
* cases and specific output formatting not covered by property generators.
*/import { describe, expect, test, mock } from "bun:test";
describe("feature", () => {
test("should return specific value", async () => {
expect(await someFunction("input")).toBe("expected output");
});
});
// Mock modules when needed
mock.module("./some-module", () => ({
default: () => "mocked",
}));| What | Where |
|---|---|
| Add new command | src/commands/<domain>/ |
| Add API types | src/types/sentry.ts |
| Add config types | src/types/config.ts |
| Add Seer types | src/types/seer.ts |
| Add utility | src/lib/ |
| Add DSN language support | src/lib/dsn/languages/ |
| Add DB operations | src/lib/db/ |
| Build scripts | script/ |
| Add property tests | test/lib/<name>.property.test.ts |
| Add model-based tests | test/lib/db/<name>.model-based.test.ts |
| Add unit tests | test/ (mirror src/ structure) |
| Add E2E tests | test/e2e/ |
| Test helpers | test/model-based/helpers.ts |
| Add documentation | docs/src/content/docs/ |
| Hand-written command doc content | docs/src/fragments/commands/ |
- Auth token env var override pattern: SENTRY_AUTH_TOKEN > SENTRY_TOKEN > SQLite: Auth in `src/lib/db/auth.ts` follows layered precedence: `SENTRY_AUTH_TOKEN` > `SENTRY_TOKEN` > SQLite OAuth token. `getEnvToken()` trims env vars (empty/whitespace = unset). `AuthSource` tracks provenance. `ENV_SOURCE_PREFIX = "env:"` — use `.length` not hardcoded 4. Env tokens bypass refresh/expiry. `isEnvTokenActive()` guards auth commands. Logout must NOT clear stored auth when env token active. These functions stay in `db/auth.ts` despite not touching DB because they're tightly coupled with token retrieval.
- Consola chosen as CLI logger with Sentry createConsolaReporter integration: Consola is the CLI logger with Sentry `createConsolaReporter` integration. Two reporters: FancyReporter (stderr) + Sentry structured logs. Level via `SENTRY_LOG_LEVEL`. `buildCommand` injects hidden `--log-level`/`--verbose` flags. `withTag()` creates independent instances; `setLogLevel()` propagates via registry. All user-facing output must use consola, not raw stderr. `HandlerContext` intentionally omits stderr.
- Input validation layer: src/lib/input-validation.ts guards CLI arg parsing: Four validators in `src/lib/input-validation.ts` guard against agent-hallucinated inputs: `rejectControlChars` (ASCII < 0x20), `rejectPreEncoded` (%XX), `validateResourceId` (rejects ?, #, %, whitespace), `validateEndpoint` (rejects `..` traversal). Applied in `parseSlashOrgProject`, bare-slug path in `parseOrgProjectArg`, `parseIssueArg`, and `normalizeEndpoint` (api.ts). NOT applied in `parseSlashSeparatedArg` for no-slash plain IDs — those may contain structural separators (newlines for log view batch IDs) that callers split downstream. Validation targets user-facing parse boundaries only; env vars and DB cache values are trusted.
- Magic @ selectors resolve issues dynamically via sort-based list API queries: Magic `@` selectors (`@latest`, `@most_frequent`) in `parseIssueArg` are detected early (before `validateResourceId`) because `@` is not in the forbidden charset. `SELECTOR_MAP` provides case-insensitive matching with common variations (`@mostfrequent`, `@most-frequent`). Resolution in `resolveSelector` (issue/utils.ts) maps selectors to `IssueSort` values (`date`, `freq`), calls `listIssuesPaginated` with `perPage: 1` and `query: 'is:unresolved'`. Supports org-prefixed form: `sentry/@latest`. Unrecognized `@`-prefixed strings fall through to suffix-only parsing (not an error). The `ParsedIssueArg` union includes `{ type: 'selector'; selector: IssueSelector; org?: string }`.
- Sentry SDK uses @sentry/node-core/light instead of @sentry/bun to avoid OTel overhead: The CLI uses `@sentry/node-core/light` instead of `@sentry/bun` to avoid loading the full OpenTelemetry stack (~150ms, 24MB). `@sentry/core` barrel is patched via `bun patch` to remove ~32 unused exports saving ~13ms. Key gotcha: `LightNodeClient` constructor hardcodes `runtime: { name: 'node' }` AFTER spreading user options, so passing `runtime` in `Sentry.init()` is silently overwritten. Fix: patch `client.getOptions().runtime` post-init (returns mutable ref). The CLI does this in `telemetry.ts` to report `bun` runtime when running as binary. Trade-offs: transport falls back to Node's `http` module instead of native `fetch`. Upstream issues: getsentry/sentry-javascript#19885 and #19886.
- Zod schema on OutputConfig enables self-documenting JSON fields in help and SKILL.md: List commands register a `schema?: ZodType` on `OutputConfig<T>` (in `src/lib/formatters/output.ts`). `extractSchemaFields()` walks Zod object shapes to produce `SchemaFieldInfo[]` (name, type, description, optional). In `command.ts`, `buildFieldsFlag()` enriches the `--fields` flag brief with available names; `enrichDocsWithSchema()` appends a fields section to `fullDescription`. The schema is exposed as `__jsonSchema` on the built command for introspection — `introspect.ts` reads it into `CommandInfo.jsonFields`. `help.ts` renders fields in `sentry help <cmd>` output. `generate-skill.ts` renders a markdown table in reference docs. For `buildOrgListCommand`/`dispatchOrgScopedList`, pass `schema` via `OrgListConfig` — `list-command.ts` forwards it to `OutputConfig`.
- All view subcommands should use <target> <id> positional pattern: All `* view` subcommands should follow a consistent `<target> <id>` positional argument pattern where target is the optional `org/project` specifier. During migration, use opportunistic argument swapping with a stderr warning when args are in wrong order. This is an instance of the broader CLI UX auto-correction pattern: safe when input is already invalid, correction is unambiguous, warning goes to stderr. Normalize at command level, keep parsers pure. Model after `gh` CLI conventions.
- Sentry CLI markdown-first formatting pipeline replaces ad-hoc ANSI: Formatters build CommonMark strings; `renderMarkdown()` renders to ANSI for TTY or raw markdown for non-TTY. Key helpers: `colorTag()`, `mdKvTable()`, `mdRow()`, `mdTableHeader()` (`:` suffix = right-aligned), `renderTextTable()`. `isPlainOutput()` checks `SENTRY_PLAIN_OUTPUT` > `NO_COLOR` > `!isTTY`. Batch path: `formatXxxTable()`. Streaming path: `StreamingTable` (TTY) or raw markdown rows (plain). Both share `buildXxxRowCells()`.
- Sentry dashboard API rejects discover/transaction-like widget types — use spans: The Sentry Dashboard API rejects `widgetType: 'discover'` and `widgetType: 'transaction-like'` as deprecated. Use `widgetType: 'spans'` for new widgets. The codebase splits types into `WIDGET_TYPES` (active, for creation) and `ALL_WIDGET_TYPES` (including deprecated, for parsing server responses). `DashboardWidgetInputSchema` must use `ALL_WIDGET_TYPES` so editing existing widgets with deprecated types passes Zod validation. `validateWidgetEnums()` in `resolve.ts` rejects deprecated types for new widget creation — but accepts `skipDeprecatedCheck: true` for the edit path, where `effectiveDataset` may inherit a deprecated type from the existing widget. Cross-validation (display vs dataset compatibility) still runs on effective values. Tests must use `error-events` instead of `discover`; it shares `DISCOVER_AGGREGATE_FUNCTIONS` including `failure_rate`.
- Sentry issue stats field: time-series controlled by groupStatsPeriod: Sentry issue stats and list table layout: `stats` key depends on `groupStatsPeriod` (`""`, `"14d"`, `"24h"`, `"auto"`); `statsPeriod` controls window. **Critical**: `count` is period-scoped — use `lifetime.count` for true total. Issue list uses `groupStatsPeriod: 'auto'` for sparklines. Columns: SHORT ID, ISSUE, SEEN, AGE, TREND, EVENTS, USERS, TRIAGE. TREND hidden < 100 cols. `--compact` tri-state: explicit overrides; `undefined` triggers `shouldAutoCompact(rowCount)` — compact if `3N + 3 > termHeight`. Height formula `3N + 3` (last row has no trailing separator).
- Sentry trace-logs API is org-scoped, not project-scoped: The Sentry trace-logs endpoint (`/organizations/{org}/trace-logs/`) is org-scoped, so `trace logs` uses `resolveOrg()` not `resolveOrgAndProject()`. The endpoint is PRIVATE in Sentry source, excluded from the public OpenAPI schema — `@sentry/api` has no generated types. The hand-written `TraceLogSchema` in `src/types/sentry.ts` is required until Sentry makes it public.
- SKILL.md is fully generated — edit source files, not output: The skill files under `plugins/sentry-cli/skills/sentry-cli/` (SKILL.md + references/*.md) are fully generated by `bun run generate:skill` (script/generate-skill.ts). CI runs this after every push via a `github-actions[bot]` commit, overwriting any manual edits. To change skill content, edit the **sources**: (1) `docs/src/content/docs/agent-guidance.md` — embedded into SKILL.md's Agent Guidance section with heading levels bumped. (2) `src/commands/*/` flag `brief` strings — generate the reference file flag descriptions. (3) `docs/src/content/docs/commands/*.md` — examples extracted per command via marked AST parsing. After editing sources, run `bun run generate:skill` locally and commit both source and generated files. CI's `bun run check:skill` fails if generated files are stale.
- Stricli route errors are uninterceptable — only post-run detection works: Stricli route errors, exit codes, and OutputError — error propagation gaps: (1) Route failures are uninterceptable — Stricli writes to stderr and returns `ExitCode.UnknownCommand` internally. Only post-`run()` `process.exitCode` check works. `exceptionWhileRunningCommand` only fires for errors in command `func()`. (2) `ExitCode.UnknownCommand` is `-5`. Bun reads `251` (unsigned byte), Node reads `-5` — compare both. (3) `OutputError` in `handleOutputError` calls `process.exit()` immediately, bypassing telemetry and `exceptionWhileRunningCommand`. Top-level typos via `defaultCommand:help` → `OutputError` → `process.exit(1)` skip all error reporting.
- Three Sentry APIs for span custom attributes with different capabilities: Three Sentry endpoints serve span data with different custom attribute support: (1) `/trace/{traceId}/` — hierarchical tree; supports `additional_attributes` query param to request named attributes (must enumerate). Returns `measurements` (web vitals, always populated with zeros for non-browser spans). (2) `/projects/{org}/{project}/trace-items/{itemId}/?trace_id={id}&item_type=spans` — single span full detail; returns ALL attributes automatically as `{name, type, value}[]`. No enumeration needed. Frontend span detail sidebar uses this. (3) `/events/?dataset=spans&field=X` — list/search; requires explicit `field` params. The CLI's `span view` calls endpoint 2 via `getSpanDetails()` with p-limit(5) concurrency. `trace view` passes `--fields` to endpoint 1 as `additional_attributes`.
- Trace view measurements filtering strips zero-valued web vitals from JSON: The `/trace/{traceId}/` endpoint returns hardcoded web vitals measurements (LCP, FCP, CLS, INP, TTFB + score ratios) on ALL spans, even non-browser ones where they're all zero. `filterSpanMeasurements()` in `trace/view.ts` recursively strips zero-valued entries from the JSON output. Non-zero measurements on root pageload spans are preserved. Uses destructuring + spread (not `delete`) to satisfy Biome's `noDelete` rule.
- withAuthGuard returns discriminated Result type, not fallback+onError: `withAuthGuard<T>(fn)` in `src/lib/errors.ts` returns a discriminated Result: `{ ok: true, value: T } | { ok: false, error: unknown }`. AuthErrors always re-throw (triggers bin.ts auto-login). All other errors are captured. Callers inspect `result.ok` to degrade gracefully. Used across 12+ files.
- Sentry-derived terminal color palette tuned for dual-background contrast: The CLI's chart/dashboard palette uses 10 colors derived from Sentry's categorical chart hues (`static/app/utils/theme/scraps/tokens/color.tsx` in getsentry/sentry), each adjusted to mid-luminance to achieve ≥3:1 contrast on both dark (#1e1e1e) and light (#f0f0f0) backgrounds. Key adjustments: orange darkened from #FF9838→#C06F20, green #67C800→#3D8F09, yellow #FFD00E→#9E8B18, purple lightened #5D3EB2→#8B6AC8, indigo #50219C→#7B50D0. Blurple (#7553FF), pink (#F0369A), magenta (#B82D90) used as-is. Teal (#228A83) added to fill a hue gap. ANSI 16-color codes were considered but rejected in favor of hex since the mid-luminance hex values provide guaranteed contrast regardless of terminal theme configuration.
- Biome lint bans process.stdout in commands — use isPlainOutput() and yield tokens instead: A custom lint rule prevents `process.stdout` usage in command files — all output must go through `yield CommandOutput` or the Stricli context's `this.stdout`. For TTY detection, use `isPlainOutput()` from `src/lib/formatters/plain-detect.ts` instead of `process.stdout.isTTY`. For ANSI control sequences (screen clear, cursor movement), yield a `ClearScreen` token and let the `buildCommand` wrapper handle it. This keeps commands decoupled from stdout details.
- Dashboard tracemetrics dataset uses comma-separated aggregate format: SDK v10+ custom metrics (, , ) emit envelope items. Dashboard widgets for these MUST use with aggregate format — e.g., . The parameter must match the SDK emission exactly: if no unit specified, for memory metrics, for uptime. only supports , , , , display types — no or . Widgets with always require . Sort expressions must reference aggregates present in .
- Dot-notation field filtering is ambiguous for keys containing dots: The `filterFields` function in `src/lib/formatters/json.ts` uses dot-notation to address nested fields (e.g., `metadata.value`). This means object keys that literally contain dots are ambiguous and cannot be addressed. Property-based tests for this function must generate field name arbitraries that exclude dots — use a restricted charset like `[a-zA-Z0-9_]` in fast-check arbitraries. Counterexample found by fast-check: `{"a":{".":false}}` with path `"a."` splits into `["a", ""]` and fails to resolve.
- Git worktree blocks branch checkout of branches used in other worktrees: `git checkout main` fails with "already used by worktree at ..." when another worktree has that branch checked out. In this repo's worktree setup, use `git checkout origin/main --detach` or create feature branches from `origin/main` directly: `git checkout -b fix/foo origin/main`. This is a standard git worktree constraint but catches people off guard in the CLI repo which uses worktrees for parallel development.
- Dashboard tracemetrics dataset uses comma-separated aggregate format: SDK v10+ custom metrics (, , ) emit envelope items. Dashboard widgets for these MUST use with aggregate format — e.g., . The parameter must match the SDK emission exactly: if no unit specified, for memory metrics, for uptime. only supports , , , , display types — no or . Widgets with always require . Sort expressions must reference aggregates present in .
- Skill eval judge needs explicit CLI context about auto-detect and flags: The skill eval judge (Haiku) reads the compact Command Reference literally. Without explicit context, it rejects valid plans because: (1) it treats `<org/project>` positional args as mandatory when they're optional via auto-detection, and (2) it rejects standard flags (`--json`, `--query`, `--limit`, `--fields`, `--period`) not shown in the compact reference. Fix: the judge prompt in `test/skill-eval/helpers/judge.ts` must include notes that positional args like `<org/project>` are optional (CLI auto-detects from DSN/config), and that commands support additional flags documented in separate reference files. Also guard against empty `commandReference` extraction in `script/eval-skill.ts` — log a warning instead of silently producing an adversarial prompt where no commands can satisfy the judge.
- spansIndexed is not a valid Sentry dataset — use spans: The Sentry Events/Explore API accepts 5 dataset values: `spans`, `transactions`, `logs`, `errors`, `discover`. The name `spansIndexed` is invalid and returns a generic HTTP 500 "Internal error" with no helpful validation message. This trips up AI agents and users. Valid datasets are documented in `src/lib/api/datasets.ts` (`EVENTS_API_DATASETS` constant) and in `docs/commands/api.md`.
- Spinner stdout/stderr collision: log messages inside withProgress appear on spinner line: The `withProgress` spinner in `src/lib/polling.ts` writes to stdout using `\r\x1b[K` (no trailing newline). Consola logger writes to stderr. On a shared terminal, any `log.info()` called **inside** the `withProgress` callback appears on the same line as the spinner text because stderr doesn't know about stdout's carriage-return positioning. Fix pattern: propagate data out of the callback via return value, then call `log.info()` **after** `withProgress` completes (when the `finally` block has already cleared the spinner line). This affected `downloadBinaryToTemp` in `upgrade.ts` where `log.info('Applied delta patch...')` fired inside the spinner callback.
- Stricli rejects unknown flags — pre-parsed global flags must be consumed from argv: Stricli's arg parser is strict: any `--flag` not registered on a command throws `No flag registered for --flag`. Global flags (parsed before Stricli in bin.ts) MUST be spliced out of argv. `--log-level` was correctly consumed but `--verbose` was intentionally left in (for the `api` command's own `--verbose`). This breaks every other command. Also, `argv.indexOf('--flag')` doesn't match `--flag=value` form — must check both space-separated and equals-sign forms when pre-parsing. A Biome `noRestrictedImports` lint rule in `biome.jsonc` now blocks `import { buildCommand } from "@stricli/core"` at error level — only `src/lib/command.ts` is exempted. Other `@stricli/core` exports (`buildRouteMap`, `run`, etc.) are allowed.
- --fields dual role: output filtering + API field selection for span list: In `span list`, `--fields` serves dual purpose: filters JSON output AND requests extra API fields. `extractExtraApiFields()` in `span/list.ts` checks each field name against `OUTPUT_TO_API_FIELD` mapping (e.g., `span_id`→`id`, `op`→`span.op`). Unknown names are treated as custom attributes and added to the `field` param in the events API request. `FIELD_GROUP_ALIASES` supports shorthand expansion (e.g., `gen_ai` → 4 gen_ai.* fields). Extra fields survive Zod via `SpanListItemSchema.passthrough()` and are forwarded by `spanListItemToFlatSpan(item, extraFieldNames)` onto `FlatSpan`'s index signature. `formatSpanTable()` dynamically adds columns for extra attributes.
- --since is an alias for --period via shared PERIOD_ALIASES: `PERIOD_ALIASES` in `src/lib/list-command.ts` maps both `t` and `since` to `period`. All commands using `LIST_PERIOD_FLAG` get `--since` as an alias for `--period` automatically via spread `...PERIOD_ALIASES`. This was added because AI agents and humans naturally try `--since 1h` instead of `--period 1h`.
- Branch naming and commit message conventions for Sentry CLI: Branch naming: `feat/<short-description>` or `fix/<issue-number>-<short-description>` (e.g., `feat/ghcr-nightly-distribution`, `fix/268-limit-auto-pagination`). Commit message format: `type(scope): description (#issue)` (e.g., `fix(issue-list): auto-paginate --limit beyond 100 (#268)`, `feat(nightly): distribute via GHCR instead of GitHub Releases`). Types seen: fix, refactor, meta, release, feat. PRs are created as drafts via `gh pr create --draft`. Implementation plans are attached to commits via `git notes add` rather than in PR body or commit message.
- ClearScreen yield token for in-place terminal refresh in buildCommand wrapper: Commands needing in-place refresh yield a `ClearScreen` token from `src/lib/formatters/output.ts`. The `handleYieldedValue` function in `buildCommand` sets a `pendingClear` flag; when the next `CommandOutput` is rendered, `renderCommandOutput` prepends `\x1b[H\x1b[J` and writes everything in a **single `stdout.write()` call** — no flicker. In JSON/plain modes the clear is silently ignored. Pattern: `yield ClearScreen()` then `yield CommandOutput(data)`. Critical: never split clear and content into separate writes. Also: never add a redundant clear-screen inside a `HumanRenderer.render()` method — the `ClearScreen` token is the sole mechanism. The dashboard renderer originally had its own `\x1b[2J\x1b[H` prepend on re-renders, causing double clears; this was removed.
- Property-based tests for input validators use stringMatching for forbidden char coverage: In `test/lib/input-validation.property.test.ts`, forbidden-character arbitraries are built with `stringMatching` targeting specific regex patterns (e.g., `/^[^\x00-\x1f]*[\x00-\x1f][^\x00-\x1f]*$/` for control chars). This ensures fast-check generates strings that always contain the forbidden character while varying surrounding content. The `biome-ignore lint/suspicious/noControlCharactersInRegex` suppression is needed on the control char regex constant in `input-validation.ts`.
- Sentry's official color token source location in getsentry/sentry repo: Sentry's canonical color palette lives in `static/app/utils/theme/scraps/tokens/color.tsx` in getsentry/sentry. It defines `categorical.light` and `categorical.dark` palettes with named colors (blurple, purple, indigo, plum, magenta, pink, salmon, orange, yellow, lime, green). Chart palettes are built in `static/app/utils/theme/theme.tsx` using `CHART_PALETTE_LIGHT` and `CHART_PALETTE_DARK` arrays that progressively add colors as series count grows (1→blurple, 6→blurple/indigo/pink/orange/yellow/green, etc.). GitHub API tree endpoint (`/git/trees/master?recursive=1`) can locate files without needing authenticated code search.
- Shared flag constants in list-command.ts for cross-command consistency: `src/lib/list-command.ts` exports shared Stricli flag definitions (`FIELDS_FLAG`, `FRESH_FLAG`, `FRESH_ALIASES`) reused across all commands. When adding a new global-ish flag to multiple commands, define it once here as a const satisfying Stricli's flag shape, then spread into each command's `flags` object. The `--fields` flag is `{ kind: 'parsed', parse: String, brief: '...', optional: true }`. `parseFieldsList()` in `formatters/json.ts` handles comma-separated parsing with trim/dedup. `writeJson()` accepts an optional `fields` array and calls `filterFields()` before serialization.
- Pagination contextKey must include all query-varying parameters with escaping: Pagination `contextKey` must encode every query-varying parameter (sort, query, period) with `escapeContextKeyValue()` (replaces `|` with `%7C`). Always provide a fallback before escaping since `flags.period` may be `undefined` in tests despite having a default: `flags.period ? escapeContextKeyValue(flags.period) : "90d"`.
- PR review workflow: reply, resolve, amend, force-push: PR review workflow: (1) Read unresolved threads via GraphQL, (2) make code changes, (3) run lint+typecheck+tests, (4) create a SEPARATE commit per review round (not amend) for incremental review, (5) push normally, (6) reply to comments via REST API, (7) resolve threads via GraphQL `resolveReviewThread`. Only amend+force-push when user explicitly asks or pre-commit hook modified files.
- Redact sensitive flags in raw argv before sending to telemetry: Telemetry context and argv redaction patterns: `withTelemetry` calls `initTelemetryContext()` BEFORE the callback — user ID, email, instance ID, runtime, and is_self_hosted tags are automatically set. For org context, read `getDefaultOrganization()` from SQLite (no API call). When sending raw argv, redact sensitive flags: `SENSITIVE_FLAGS` in `telemetry.ts` (currently `token`). Scan for `--token`/`-token`, replace following value with `[REDACTED]`. Handle both `--flag value` and `--flag=value` forms. `setFlagContext` handles parsed flags separately.
- Stricli optional boolean flags produce tri-state (true/false/undefined): Stricli boolean flags with `optional: true` (no `default`) produce `boolean | undefined` in the flags type. `--flag` → `true`, `--no-flag` → `false`, omitted → `undefined`. This enables auto-detect patterns: explicit user choice overrides, `undefined` triggers heuristic. Used by `--compact` on issue list. The flag type must be `readonly field?: boolean` (not `readonly field: boolean`). This differs from `default: false` which always produces a defined boolean.
- Testing Stricli command func() bodies via spyOn mocking: Stricli/Bun test patterns: (1) Command func tests: `const func = await cmd.loader()`, then `func.call(mockContext, flags, ...args)`. `loader()` return type union causes LSP errors — false positives that pass `tsc`. File naming: `*.func.test.ts`. (2) ESM prevents `vi.spyOn` on Node built-in exports. Workaround: test subclass that overrides the method calling the built-in. (3) Follow-mode uses `setTimeout`-based scheduling; test with `interceptSigint()` helper. `Bun.sleep()` has no AbortSignal so `setTimeout`/`clearTimeout` required.
- validateWidgetEnums skipDeprecatedCheck for edit-path inherited datasets: When editing a widget, `effectiveDataset = flags.dataset ?? existing.widgetType` may inherit a deprecated type (e.g., `discover`). The `validateWidgetEnums` deprecation check must be skipped for inherited values — only fire when the user explicitly passes `--dataset`. Solution: `validateWidgetEnums(effectiveDisplay, effectiveDataset, { skipDeprecatedCheck: true })` in `edit.ts`. The cross-validation between display type and dataset still runs on effective values, catching incompatible combos. The deprecation rejection helper `rejectInvalidDataset()` is extracted to keep `validateWidgetEnums` under Biome's complexity limit of 15.
- SKILL.md generator must filter hidden Stricli flags: `script/generate-skill.ts` introspects Stricli's route tree to auto-generate SKILL.md. `FlagDef` must include `hidden?: boolean`; `extractFlags` propagates it so `generateCommandDoc` filters out hidden flags alongside `help`/`helpAll`. Hidden flags from `buildCommand` (`--log-level`, `--verbose`) appear globally in `docs/src/content/docs/commands/index.md` Global Options section, pulled into SKILL.md via `loadCommandsOverview`. When `cmd.jsonFields` is present (from Zod schema registration), `generateFullCommandDoc` renders a markdown "JSON Fields" table with field name, type, and description columns in reference docs.