Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
25 changes: 23 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,15 @@ mock.module("./some-module", () => ({

### Architecture

<!-- lore:019dabe5-3eee-73a9-83b4-edc56734696a -->
* **env-registry.ts drives --help env var section + docs**: \`src/lib/env-registry.ts\` (\`ENV\_VAR\_REGISTRY\`) is the single source for all env vars the CLI honors. Entries have \`{name, description, example?, defaultValue?, installOnly?, topLevel?, briefDescription?}\`. \`topLevel: true\` + \`briefDescription\` surfaces in \`sentry --help\` Environment Variables section (via \`formatEnvVarsSection()\` in \`help.ts\`) and in \`sentry help --json\` as \`envVars\` array on the full-tree envelope. Docs generator consumes the full registry for \`configuration.md\`. When adding a new env var, add it here with \`installOnly: true\` if install-script-only. Reserve \`topLevel: true\` for core-path vars only (auth, targeting, URL, key display/logging).

<!-- lore:019da557-63d5-7c8a-9ce7-54e992f312ec -->
* **Sentry log IDs are UUIDv7 — enables deterministic retention checks**: Sentry log IDs are UUIDv7 (first 12 hex = ms timestamp, version char \`7\` at pos 13). Traces/event IDs are NOT v7. \`decodeUuidV7Timestamp()\` and \`ageInDaysFromUuidV7()\` in \`src/lib/hex-id.ts\` return null for non-v7, safe to call unconditionally. Enables deterministic 'past retention' messages; wired in \`recoverHexId\` and \`log/view.ts#throwNotFoundError\`. \`RETENTION\_DAYS.log = 90\` in \`src/lib/retention.ts\`; traces/events are \`null\` (plan-dependent). \`LOG\_RETENTION\_PERIOD\` is DERIVED as \`\` \`${RETENTION\_DAYS.log}d\` \`\` — never hardcode \`'90d'\`. Shared hex primitives (\`HEX\_ID\_RE\`, \`SPAN\_ID\_RE\`, \`UUID\_DASH\_RE\`, etc.) live in \`hex-id.ts\`.

<!-- lore:019d4a08-22c3-765b-ba12-d91b29e9d497 -->
* **Three Sentry APIs for span custom attributes with different capabilities**: \*\*Three Sentry span APIs with different capabilities\*\*: (1) \`/trace/{traceId}/\` — hierarchical tree with \`additional\_attributes\`. (2) \`/projects/{org}/{project}/trace-items/{itemId}/\` — single span with ALL attributes. (3) \`/events/?dataset=spans\&field=X\` — list/search. Critical: \`meta.fields\` order is non-deterministic — derive column order from user's \`--field\`/\`-F\` list, not \`Object.keys()\`. See \`orderFieldNames()\` in \`explore.ts\`.

<!-- lore:019da6b7-1d7b-70b0-adf8-769712f5c577 -->
* **Issue resolve --in grammar: release + @next + @commit sentinels**: \*\*Issue resolve --in grammar + repo\_cache SQLite table\*\*: \`sentry issue resolve --in\` grammar: (a) omitted→immediate, (b) \`\<version>\`→\`inRelease\`, (c) \`@next\`→\`inNextRelease\`, (d) \`@commit\`→auto-detect git HEAD via \`src/lib/git.ts\`, (e) \`@commit:\<repo>@\<sha>\`→explicit. \`parseResolveSpec\` splits on LAST \`@\` for scoped names. API requires \`statusDetails.inCommit: {commit, repository}\` — not bare SHA. Repo matching uses \`listRepositoriesCached(org)\` (7-day SQLite cache in \`repo\_cache\` table, schema v14). Always use \`listAllRepositories\` (paginated via \`API\_MAX\_PER\_PAGE\`) — never \`listRepositories\` (silently caps ~25). \`setCachedRepos\` wrapped in try/catch so read-only DBs (macOS \`sudo brew install\`) don't crash commands.

Expand All @@ -1084,12 +1093,18 @@ mock.module("./some-module", () => ({

### Gotcha

<!-- lore:019db57b-ba0d-7a5f-803d-f15b7a819d05 -->
* **--json schema stability: collapse=organization drops nested org fields**: --json schema + response cache gotchas: (1) \`?collapse=organization\` shrinks \`organization\` to \`{id, slug}\` — silent --json regression. \`jsonTransform\` re-hydrates \`organization.name\` via \`resolveOrgDisplayName\` against \`org\_regions\` cache. (2) \`buildCacheKey()\` normalizes URL with sorted query params, so \`invalidateCachedResponse(baseUrl)\` misses entries with query suffixes. Use \`invalidateCachedResponsesMatching(prefix)\` (raw \`startsWith()\`); \`buildApiUrl()\` always emits trailing slash → safe prefix. (3) When \`jsonTransform\` is set, \`jsonExclude\` and \`filterFields\` are NOT applied — transform must call \`filterFields(result, fields)\` and omit excluded keys itself.
<!-- lore:019dd024-464d-74e7-b637-c6b87a9d2082 -->
* **api.ts: plain Error throws inside func() bypass CliError handling**: \*\*api.ts: plain Error throws inside func() bypass CliError handling\*\*: \`src/commands/api.ts\` throws plain \`new Error(...)\` in validation paths called from \`func()\` — this bypasses \`app.ts\`'s \`instanceof CliError\` check, causing user to see stack traces AND Sentry bug reports. Fix: use \`ValidationError\` for user-input errors inside \`func()\`. Plain \`Error\` is only OK in Stricli \`parse:\` callbacks where Stricli catches them.

<!-- lore:019da644-b93f-776d-843d-05c3c1d3a193 -->
* **Biome lint differs between local lint:fix and CI lint**: \*\*Biome lint differs between local lint:fix and CI lint\*\*: \`lint:fix\` hides CI issues; always run \`bun run lint\` before pushing. Key gotchas: (1) \`noPrecisionLoss\` on int >2^53 — use \`Number(string)\`. (2) \`noIncrementDecrement\` — use \`i += 1\`. (3) \`noExcessiveCognitiveComplexity\` caps at 15 — extract helpers, don't biome-ignore. (4) \`noUselessUndefined\` then \`noEmptyBlockStatements\` — use \`function noop() {}\`. (5) Plugin forbids raw \`metadata\` table queries — use \`getMetadata\`/\`setMetadata\`/\`clearMetadata\`. Also enforced: \`useBlockStatements\`, \`noNestedTernary\`, \`useAtIndex\`, \`noStaticOnlyClass\`.

<!-- lore:019dd588-df59-71ab-aad6-7453c4a99ccb -->
* **API tests must use useTestConfigDir to isolate disk response cache**: \*\*API tests must use useTestConfigDir to isolate disk response cache\*\*: Tests mocking \`globalThis.fetch\` MUST call \`useTestConfigDir()\` + \`setAuthToken()\`. \`authenticatedFetch\` checks a filesystem response cache (\`~/.sentry/cache/responses/\`) BEFORE calling fetch — without per-test dirs, test N's response is served to test N+1. TTL tiers in \`classifyUrl()\`: stable=5min, volatile=60s (issues/logs), immutable=24h (events/traces by ID). Also: \`@sentry/api\` SDK calls \`\_fetch(request)\` with no init — fall back to \`input.headers\` when \`init\` is undefined (prevents HTTP 415). SDK returns \`data={}\` for empty/204 responses — always guard with \`Array.isArray(data)\` before \`.map()\`. Use \`unwrapPaginatedResult\` (not \`unwrapResult\`) for Link header pagination.

<!-- lore:019dc0ef-bb36-7230-be5d-56b536a6de8e -->
* **buildCommand wrapper: loader() returns wrapped async fn, not the generator**: \*\*buildCommand wrapper: loader() returns wrapped async fn, not generator\*\*: \`cmd.loader()\` returns the wrapped async fn, not \`async \*func()\`. Wrapper iterates generator internally and writes to \`ctx.stdout\`. Tests: \`await func.call(ctx, flags, ...args)\` like a promise — don't iterate. Auth guard runs first; \`test/preload.ts:100\` sets fake \`SENTRY\_AUTH\_TOKEN\`. Tests must save/restore only env vars they mutate.

<!-- lore:019dc095-9ce4-7fe1-89ab-12efeffddcee -->
* **Biome noUselessUndefined also rejects () => {} empty arrow callbacks**: Biome lint traps: (1) \`noUselessUndefined\` rejects \`() => undefined\` AND \`noEmptyBlockStatements\` rejects \`() => {}\` — use top-level \`function noop(): void {}\`. (2) \`noExcessiveCognitiveComplexity\` caps at 15. (3) \`expect(() => fn()).toThrow(X)\` must be one line. (4) Plugin forbids raw \`metadata\` table queries — use \`getMetadata\`/\`setMetadata\`/\`clearMetadata\`. (5) Also enforced: \`useBlockStatements\`, \`noNestedTernary\`, \`useAtIndex\`, \`noStaticOnlyClass\`, \`useSimplifiedLogicExpression\`, \`noShadow\`. Namespace imports forbidden. (6) \`useYield\` fires on \`async \*func()\` with statements but not empty bodies — only add \`biome-ignore\` to generators with statements. \`lint:fix\` differs from CI \`lint\`: auto-fix hides \`noPrecisionLoss\` on >2^53 literals, \`noIncrementDecrement\`, import ordering. Always \`bun run lint\` before pushing.

Expand All @@ -1110,6 +1125,12 @@ mock.module("./some-module", () => ({

### Pattern

<!-- lore:019dc053-2e98-7b93-80e0-dee06710e849 -->
* **Merging mock.module() test files with static-import counterparts**: \*\*Bun test mocking traps\*\*: (1) \`mock.module()\` for CJS built-ins needs \`default\` re-export + named exports, declared top-level BEFORE \`await import()\`. (2) Convert code-under-test to \`await import()\` when merging mocks — pre-existing static imports won't re-bind. (3) Destructured imports capture binding at load. (4) \`Bun.mmap()\` always PROT\_WRITE — use \`new Uint8Array(await Bun.file(path).arrayBuffer())\` for read-only.

<!-- lore:019dd2ff-f956-7c25-80bd-486c57c2297a -->
* **URL-encoded paren assertions: decode before contains-check**: \*\*URL-encoded paren assertions in tests\*\*: Aggregate field names like \`count()\` become \`count%28%29\` via \`encodeURIComponent\` — use \`expect(decodeURIComponent(url)).toContain("field=count()")\`. Sentry pagination Link header format: \`\<url>; rel="next"; cursor="0:50:0"\` — cursor is in a separate attribute, NOT in URL query. Use \`parseSentryLinkHeader()\` from \`src/lib/api/infrastructure.ts\` to extract.

<!-- lore:019dba09-70b6-7862-b051-362ae1631959 -->
* **CLI-1D3 Windows download visibility race: poll statSync with exponential backoff**: Windows upgrade download visibility race: \`waitForBinaryVisible\` in \`src/lib/upgrade.ts\` polls \`statSync\` with exponential backoff (6 attempts, 5 sleeps: 100+200+400+800+1600ms). Loop breaks BEFORE final sleep — \`VERIFY\_MAX\_ATTEMPTS=N\` yields N-1 sleeps (off-by-one trap). Covers Bun 1.3.9 race where \`Bun.file().writer().end()\` returns before OS surfaces file by path. \`isEnoentSpawnError()\` in \`src/commands/cli/upgrade.ts\` catches both \`code==='ENOENT'\` and Bun's path-string error → \`UpgradeError('execution\_failed')\`. Race-free tests: writer must poll until bad state exists, then overwrite.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ cli/
│ │ ├── auth/ # login, logout, refresh, status, token, whoami
│ │ ├── cli/ # defaults, feedback, fix, setup, upgrade
│ │ ├── dashboard/ # list, view, create, add, edit, delete
│ │ ├── event/ # view, list
│ │ ├── event/ # view, list, send
│ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, archive, merge
│ │ ├── log/ # list, view
│ │ ├── org/ # list, view
Expand Down
58 changes: 58 additions & 0 deletions docs/src/fragments/commands/event.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@



## Examples

### Sending Events

```bash
# Send an error event (default level)
sentry event send -m "Something went wrong"

# Specify level, release, and environment
sentry event send -m "Deploy check" -l info -r 1.0.0 -E production

# Add tags and extra data
sentry event send -m "Payment failed" --tag env:prod --tag region:us-east --extra amount:99.99

# Set user context
sentry event send -m "Login error" --user id:42 --user email:alice@example.com

# Custom fingerprint to group related events together
sentry event send -m "DB timeout" --fingerprint db-timeout --fingerprint {{ default }}
```

### Send from a JSON file

```bash
# Send a serialized Sentry Event object
sentry event send ./crash.json

# Send without re-parsing (raw mode — also supports pre-built envelopes)
sentry event send --raw ./crash.json
sentry event send --raw ./captured.envelope
```

### DSN authentication

`sentry event send` authenticates via a **DSN** rather than a user token.
No `sentry auth login` is required.

The DSN is resolved in priority order:

1. `--dsn <value>` flag (explicit)
2. `SENTRY_DSN` environment variable

```bash
# Explicit DSN
sentry event send -m "Test" --dsn "https://key@o123.ingest.us.sentry.io/456"

# Via environment variable
export SENTRY_DSN="https://key@o123.ingest.us.sentry.io/456"
sentry event send -m "Test"
```

### Listing Events

```bash
Expand Down Expand Up @@ -68,3 +118,11 @@ Event IDs can be found:
1. In the Sentry UI when viewing an issue's events
2. In the output of `sentry issue view` commands
3. In error reports sent to Sentry (as `event_id`)

## Backward compatibility

The old sentry-cli top-level command is available as a hidden alias:

```bash
sentry send-event # same as: sentry event send
```
3 changes: 2 additions & 1 deletion plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,11 @@ Manage Sentry issues

### Event

View and list Sentry events
View, list, and send Sentry events

- `sentry event view <org/project/event-id...>` — View details of one or more events
- `sentry event list <issue>` — List events for an issue
- `sentry event send <args...>` — Send a Sentry event

→ Full flags and examples: `references/event.md`

Expand Down
60 changes: 58 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/references/event.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
name: sentry-cli-event
version: 0.32.0-dev.0
description: View and list Sentry events
description: View, list, and send Sentry events
requires:
bins: ["sentry"]
auth: true
---

# Event Commands

View and list Sentry events
View, list, and send Sentry events

### `sentry event view <org/project/event-id...>`

Expand Down Expand Up @@ -87,4 +87,60 @@ sentry event list PROJ-ABC -c prev
sentry event list PROJ-ABC --json
```

### `sentry event send <args...>`

Send a Sentry event

**Flags:**
- `--dsn <value> - DSN to send events to (overrides SENTRY_DSN env var)`
- `-m, --message <value>... - Event message (repeat for multi-line)`
- `-a, --message-arg <value>... - Arguments for message template (repeat for multiple)`
- `-l, --level <value> - Event severity level - (default: "error")`
- `-r, --release <value> - Release version`
- `-d, --dist <value> - Distribution identifier`
- `-E, --env <value> - Environment name (e.g. production, staging)`
- `-p, --platform <value> - Platform identifier (default: other)`
- `-t, --tag <value>... - Tag as KEY:VALUE (repeat for multiple)`
- `-e, --extra <value>... - Extra data as KEY:VALUE (repeat for multiple)`
- `-u, --user <value>... - User info as KEY:VALUE — id, email, username, ip_address, or custom`
- `-f, --fingerprint <value>... - Custom fingerprint part (repeat for multiple)`
- `--timestamp <value> - Event timestamp (Unix epoch, ISO 8601, or RFC 2822)`
- `--no-environ - Do not include environment variables in the event`
- `--raw - Send file contents as-is without parsing`

**Examples:**

```bash
# Send an error event (default level)
sentry event send -m "Something went wrong"

# Specify level, release, and environment
sentry event send -m "Deploy check" -l info -r 1.0.0 -E production

# Add tags and extra data
sentry event send -m "Payment failed" --tag env:prod --tag region:us-east --extra amount:99.99

# Set user context
sentry event send -m "Login error" --user id:42 --user email:alice@example.com

# Custom fingerprint to group related events together
sentry event send -m "DB timeout" --fingerprint db-timeout --fingerprint {{ default }}

# Send a serialized Sentry Event object
sentry event send ./crash.json

# Send without re-parsing (raw mode — also supports pre-built envelopes)
sentry event send --raw ./crash.json
sentry event send --raw ./captured.envelope

# Explicit DSN
sentry event send -m "Test" --dsn "https://key@o123.ingest.us.sentry.io/456"

# Via environment variable
export SENTRY_DSN="https://key@o123.ingest.us.sentry.io/456"
sentry event send -m "Test"

sentry send-event # same as: sentry event send
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
7 changes: 7 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { listCommand as replayListCommand } from "./commands/replay/list.js";
import { repoRoute } from "./commands/repo/index.js";
import { listCommand as repoListCommand } from "./commands/repo/list.js";
import { schemaCommand } from "./commands/schema.js";
import { sendEnvelopeCommand } from "./commands/send-envelope.js";
import { sendEventCommand } from "./commands/send-event.js";
import { sourcemapRoute } from "./commands/sourcemap/index.js";
import { spanRoute } from "./commands/span/index.js";
import { listCommand as spanListCommand } from "./commands/span/list.js";
Expand Down Expand Up @@ -105,6 +107,9 @@ export const routes = buildRouteMap({
init: initCommand,
api: apiCommand,
schema: schemaCommand,
// Backward-compat aliases for old sentry-cli — hidden from help
"send-event": sendEventCommand,
"send-envelope": sendEnvelopeCommand,
dashboards: dashboardListCommand,
issues: issueListCommand,
orgs: orgListCommand,
Expand Down Expand Up @@ -141,6 +146,8 @@ export const routes = buildRouteMap({
trials: true,
sourcemaps: true,
whoami: true,
"send-event": true,
"send-envelope": true,
},
},
});
Expand Down
9 changes: 6 additions & 3 deletions src/commands/event/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { buildRouteMap } from "../../lib/route-map.js";
import { listCommand } from "./list.js";
import { sendCommand } from "./send.js";
import { viewCommand } from "./view.js";

export const eventRoute = buildRouteMap({
routes: {
view: viewCommand,
list: listCommand,
send: sendCommand,
},
defaultCommand: "view",
docs: {
brief: "View and list Sentry events",
brief: "View, list, and send Sentry events",
fullDescription:
"View and list event data from Sentry.\n\n" +
"View, list, and send event data from Sentry.\n\n" +
"Use 'sentry event view <event-id>' to view a specific event.\n" +
"Use 'sentry event list <issue-id>' to list events for an issue.",
"Use 'sentry event list <issue-id>' to list events for an issue.\n" +
"Use 'sentry event send -m <message>' to send a test event.",
hideRoute: {},
},
});
Loading
Loading