Skip to content

Commit 8de58e3

Browse files
committed
fix(logger): inject --verbose and --log-level as proper Stricli flags
Fix three bugs in the logging system introduced in #338: 1. `--verbose` rejected by all commands except `api` — Stricli throws "No flag registered for --verbose" because extractLogLevelFromArgs intentionally left it in argv for the api command. 2. `--log-level=debug` (equals form) not handled — argv.indexOf only matched the space-separated form, passing the flag through to Stricli. 3. `SENTRY_LOG_LEVEL` env var has no visible effect — consola withTag() creates independent instances that snapshot the level at creation time. Module-level scoped loggers never saw later setLogLevel() calls. Instead of pre-parsing flags from argv (bypassing Stricli), define them as proper hidden Stricli flags. buildCommand in src/lib/command.ts now wraps Stricli to inject hidden --log-level (enum) and --verbose (boolean) flags into every command, intercept them, apply setLogLevel(), then strip them before calling the original func. When a command already defines its own --verbose (e.g. api uses it for HTTP output), the injected one is skipped — the command own value still triggers debug-level logging as a side-effect. setLogLevel() now propagates to all withTag() children via a registry, fixing the env var and flag having no effect on scoped loggers. Document SENTRY_LOG_LEVEL, --log-level, and --verbose in configuration docs.
1 parent 22071f1 commit 8de58e3

File tree

9 files changed

+622
-274
lines changed

9 files changed

+622
-274
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ mock.module("./some-module", () => ({
686686
* **Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag**: List commands with cursor pagination use \`buildPaginationContextKey(type, identifier, flags)\` for composite context keys and \`parseCursorFlag(value)\` accepting \`"last"\` magic value. Critical: \`resolveCursor()\` must be called inside the \`org-all\` override closure, not before \`dispatchOrgScopedList\` — otherwise cursor validation errors fire before the correct mode-specific error.
687687
688688
<!-- lore:019cbd5f-ec35-7e2d-8386-6d3a67adf0cf -->
689-
* **Telemetry instrumentation pattern: withTracingSpan + captureException for handled errors**: For graceful-fallback operations, use \`withTracingSpan\` from \`src/lib/telemetry.ts\` for child spans and \`captureException\` from \`@sentry/bun\` (named import — Biome forbids namespace imports) with \`level: 'warning'\` for non-fatal errors. \`withTracingSpan\` uses \`onlyIfParent: true\` so it's a no-op without active transaction. When returning \`withTracingSpan(...)\` directly, drop \`async\` and use \`Promise.resolve(null)\` for early returns. User-visible fallbacks should use \`log.warn()\` not \`log.debug()\` — debug is invisible at default level. Also: several commands bypass telemetry by importing \`buildCommand\` from \`@stricli/core\` directly instead of \`../../lib/command.js\`. Affected: trace/list, trace/view, log/view, api.ts, help.ts.
689+
* **Telemetry instrumentation pattern: withTracingSpan + captureException for handled errors**: For graceful-fallback operations, use \`withTracingSpan\` from \`src/lib/telemetry.ts\` for child spans and \`captureException\` from \`@sentry/bun\` (named import — Biome forbids namespace imports) with \`level: 'warning'\` for non-fatal errors. \`withTracingSpan\` uses \`onlyIfParent: true\` so it's a no-op without active transaction. When returning \`withTracingSpan(...)\` directly, drop \`async\` and use \`Promise.resolve(null)\` for early returns. User-visible fallbacks should use \`log.warn()\` not \`log.debug()\` — debug is invisible at default level. \`buildCommand\` in \`src/lib/command.ts\` wraps Stricli's \`buildCommand\` to inject hidden \`--log-level\` (enum) and \`--verbose\` (boolean) flags, telemetry capture, and logging setup into every command. When a command already defines its own \`--verbose\` (e.g. \`api\` uses it for HTTP output), the injected one is skipped — the command's own value still triggers debug-level logging as a side-effect. Consola's \`withTag()\` creates independent instances — \`setLogLevel()\` propagates to all children via a registry.
690690
691691
### Preference
692692

docs/src/content/docs/configuration.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ Disable CLI telemetry (error tracking for the CLI itself). The CLI sends anonymi
8989
export SENTRY_CLI_NO_TELEMETRY=1
9090
```
9191

92+
### `SENTRY_LOG_LEVEL`
93+
94+
Controls the verbosity of diagnostic output. Defaults to `info`.
95+
96+
Valid values: `error`, `warn`, `log`, `info`, `debug`, `trace`
97+
98+
```bash
99+
export SENTRY_LOG_LEVEL=debug
100+
```
101+
102+
Equivalent to passing `--log-level debug` on the command line. CLI flags take precedence over the environment variable.
103+
92104
### `SENTRY_CLI_NO_UPDATE_CHECK`
93105

94106
Disable the automatic update check that runs periodically in the background.
@@ -97,6 +109,33 @@ Disable the automatic update check that runs periodically in the background.
97109
export SENTRY_CLI_NO_UPDATE_CHECK=1
98110
```
99111

112+
## Global Options
113+
114+
These flags are accepted by every command. They are not shown in individual command `--help` output, but are always available.
115+
116+
### `--log-level <level>`
117+
118+
Set the log verbosity level. Accepts: `error`, `warn`, `log`, `info`, `debug`, `trace`.
119+
120+
```bash
121+
sentry issue list --log-level debug
122+
sentry --log-level=trace cli upgrade
123+
```
124+
125+
Overrides `SENTRY_LOG_LEVEL` when both are set.
126+
127+
### `--verbose`
128+
129+
Shorthand for `--log-level debug`. Enables debug-level diagnostic output.
130+
131+
```bash
132+
sentry issue list --verbose
133+
```
134+
135+
:::note
136+
The `sentry api` command also uses `--verbose` to show full HTTP request/response details. When used with `sentry api`, it serves both purposes (debug logging + HTTP output).
137+
:::
138+
100139
## Credential Storage
101140

102141
Credentials are stored in a SQLite database at `~/.sentry/` (or the path set by `SENTRY_CONFIG_DIR`) with restricted file permissions (mode 600) for security. The database also caches:

src/bin.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { buildContext } from "./context.js";
55
import { AuthError, formatError, getExitCode } from "./lib/errors.js";
66
import { error } from "./lib/formatters/colors.js";
77
import { runInteractiveLogin } from "./lib/interactive-login.js";
8-
import {
9-
extractLogLevelFromArgs,
10-
getEnvLogLevel,
11-
setLogLevel,
12-
} from "./lib/logger.js";
8+
import { getEnvLogLevel, setLogLevel } from "./lib/logger.js";
139
import { withTelemetry } from "./lib/telemetry.js";
1410
import { startCleanupOldBinary } from "./lib/upgrade.js";
1511
import {
@@ -94,22 +90,14 @@ async function main(): Promise<void> {
9490

9591
const args = process.argv.slice(2);
9692

97-
// Apply SENTRY_LOG_LEVEL env var first (lazy read, not at module load time).
98-
// CLI flags below override this if present.
93+
// Apply SENTRY_LOG_LEVEL env var early (lazy read, not at module load time).
94+
// CLI flags (--log-level, --verbose) are handled by Stricli via
95+
// buildCommand and take priority when present.
9996
const envLogLevel = getEnvLogLevel();
10097
if (envLogLevel !== null) {
10198
setLogLevel(envLogLevel);
10299
}
103100

104-
// Extract global log-level flags before Stricli parses args.
105-
// --log-level is consumed (removed); --verbose is read but left in place
106-
// because some commands (e.g., `api`) define their own --verbose flag.
107-
// CLI flags take priority over SENTRY_LOG_LEVEL env var.
108-
const logLevel = extractLogLevelFromArgs(args);
109-
if (logLevel !== null) {
110-
setLogLevel(logLevel);
111-
}
112-
113101
const suppressNotification = shouldSuppressNotification(args);
114102

115103
// Start background update check (non-blocking)

src/commands/help.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* - `sentry help <command>`: Shows Stricli's detailed help (--helpAll) for that command
77
*/
88

9-
import { buildCommand, run } from "@stricli/core";
9+
import { run } from "@stricli/core";
1010
import type { SentryContext } from "../context.js";
11+
import { buildCommand } from "../lib/command.js";
1112
import { printCustomHelp } from "../lib/help.js";
1213

1314
export const helpCommand = buildCommand({

src/lib/command.ts

Lines changed: 147 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
/**
2-
* Command Builder with Telemetry
2+
* Command builder with telemetry and global logging flag injection.
33
*
4-
* Wraps Stricli's buildCommand to automatically capture flag usage for telemetry.
4+
* Provides `buildCommand` — the standard command builder for all Sentry CLI
5+
* commands. It wraps Stricli's `buildCommand` with:
56
*
6-
* ALL commands MUST import `buildCommand` from this module, NOT from `@stricli/core`.
7-
* Importing directly from `@stricli/core` silently bypasses flag/arg telemetry capture.
7+
* 1. **Automatic flag/arg telemetry** — captures flag values and positional
8+
* arguments as Sentry span context for observability.
89
*
9-
* Correct: import { buildCommand } from "../../lib/command.js";
10-
* Incorrect: import { buildCommand } from "@stricli/core"; // skips telemetry!
10+
* 2. **Hidden global logging flags** — injects `--log-level` and `--verbose`
11+
* into every command's parameters. These are intercepted before the original
12+
* `func` runs: the logger level is set, and the injected flags are stripped
13+
* so the original function never sees them. If a command already defines its
14+
* own `--verbose` flag (e.g. `api` uses it for HTTP output), the injected
15+
* one is skipped and the command's own value is used for both purposes.
16+
*
17+
* ALL commands MUST use `buildCommand` from this module, NOT from
18+
* `@stricli/core`. Importing directly from Stricli silently bypasses
19+
* telemetry and global flag handling.
1120
*
12-
* Exception: `help.ts` may import from `@stricli/core` because it also needs `run`,
13-
* and the help command has no meaningful flags to capture.
21+
* ```
22+
* Correct: import { buildCommand } from "../../lib/command.js";
23+
* Incorrect: import { buildCommand } from "@stricli/core"; // skips everything!
24+
* ```
1425
*/
1526

1627
import {
@@ -20,6 +31,12 @@ import {
2031
buildCommand as stricliCommand,
2132
numberParser as stricliNumberParser,
2233
} from "@stricli/core";
34+
import {
35+
LOG_LEVEL_NAMES,
36+
type LogLevelName,
37+
parseLogLevel,
38+
setLogLevel,
39+
} from "./logger.js";
2340
import { setArgsContext, setFlagContext } from "./telemetry.js";
2441

2542
/**
@@ -54,23 +71,81 @@ type LocalCommandBuilderArguments<
5471
readonly func: CommandFunction<FLAGS, ARGS, CONTEXT>;
5572
};
5673

74+
// ---------------------------------------------------------------------------
75+
// Global logging flags
76+
// ---------------------------------------------------------------------------
77+
5778
/**
58-
* Build a command with automatic flag telemetry.
79+
* Hidden `--log-level` flag injected into every command by {@link buildCommand}.
5980
*
60-
* This is a drop-in replacement for Stricli's buildCommand that wraps the
61-
* command function to automatically capture flag values as Sentry tags.
81+
* Accepts one of the valid log level names. Hidden so it doesn't clutter
82+
* individual command `--help` output — it's documented at the CLI level.
83+
*/
84+
export const LOG_LEVEL_FLAG = {
85+
kind: "enum" as const,
86+
values: LOG_LEVEL_NAMES as unknown as LogLevelName[],
87+
brief: "Set log verbosity level",
88+
optional: true as const,
89+
hidden: true as const,
90+
} as const;
91+
92+
/**
93+
* Hidden `--verbose` flag injected into every command by {@link buildCommand}.
94+
* Equivalent to `--log-level debug`.
95+
*/
96+
export const VERBOSE_FLAG = {
97+
kind: "boolean" as const,
98+
brief: "Enable verbose (debug-level) logging output",
99+
default: false,
100+
hidden: true as const,
101+
} as const;
102+
103+
/** The flag key for the injected --log-level flag (always stripped) */
104+
const LOG_LEVEL_KEY = "log-level";
105+
106+
/**
107+
* Apply logging flags parsed by Stricli.
62108
*
63-
* Usage is identical to Stricli's buildCommand - just change the import:
64-
* ```ts
65-
* // Before:
66-
* import { buildCommand } from "@stricli/core";
109+
* `--log-level` takes priority over `--verbose`. If neither is specified,
110+
* the level is left as-is (env var or default).
67111
*
68-
* // After:
69-
* import { buildCommand } from "../../lib/command.js";
70-
* ```
112+
* @param logLevel - Value of the `--log-level` flag, if provided
113+
* @param verbose - Value of the `--verbose` flag
114+
*/
115+
export function applyLoggingFlags(
116+
logLevel: LogLevelName | undefined,
117+
verbose: boolean
118+
): void {
119+
if (logLevel) {
120+
setLogLevel(parseLogLevel(logLevel));
121+
} else if (verbose) {
122+
setLogLevel(parseLogLevel("debug"));
123+
}
124+
}
125+
126+
// ---------------------------------------------------------------------------
127+
// buildCommand — the single entry point for all Sentry CLI commands
128+
// ---------------------------------------------------------------------------
129+
130+
/**
131+
* Build a Sentry CLI command with telemetry and global logging flags.
132+
*
133+
* This is the **only** command builder that should be used. It:
134+
* 1. Injects hidden `--log-level` and `--verbose` flags into the parameters
135+
* 2. Intercepts them before the original `func` runs to call `setLogLevel()`
136+
* 3. Strips injected flags so the original function never sees them
137+
* 4. Captures flag values and positional arguments as Sentry telemetry context
138+
*
139+
* When a command already defines its own `verbose` flag (e.g. the `api` command
140+
* uses `--verbose` for HTTP request/response output), the injected `VERBOSE_FLAG`
141+
* is skipped. The command's own `verbose` value is still used for log-level
142+
* side-effects, and it is **not** stripped — the original func receives it as usual.
71143
*
72-
* @param builderArgs - Same arguments as Stricli's buildCommand
73-
* @returns A Command with automatic flag telemetry
144+
* Flag keys use kebab-case because Stricli uses the literal object key as
145+
* the CLI flag name (e.g. `"log-level"` → `--log-level`).
146+
*
147+
* @param builderArgs - Same shape as Stricli's buildCommand arguments
148+
* @returns A fully-wrapped Stricli Command
74149
*/
75150
export function buildCommand<
76151
const FLAGS extends BaseFlags = NonNullable<unknown>,
@@ -81,27 +156,69 @@ export function buildCommand<
81156
): Command<CONTEXT> {
82157
const originalFunc = builderArgs.func;
83158

84-
// Wrap the function to capture flags and args before execution
85-
const wrappedFunc = function (
86-
this: CONTEXT,
87-
flags: FLAGS,
88-
...args: ARGS
89-
): ReturnType<typeof originalFunc> {
159+
// Merge logging flags into the command's flag definitions.
160+
// Quoted keys produce kebab-case CLI flags: "log-level" → --log-level
161+
const existingParams = (builderArgs.parameters ?? {}) as Record<
162+
string,
163+
unknown
164+
>;
165+
const existingFlags = (existingParams.flags ?? {}) as Record<string, unknown>;
166+
167+
// If the command already defines --verbose (e.g. api command), don't override it.
168+
const commandOwnsVerbose = "verbose" in existingFlags;
169+
170+
const mergedFlags: Record<string, unknown> = {
171+
...existingFlags,
172+
[LOG_LEVEL_KEY]: LOG_LEVEL_FLAG,
173+
};
174+
if (!commandOwnsVerbose) {
175+
mergedFlags.verbose = VERBOSE_FLAG;
176+
}
177+
const mergedParams = { ...existingParams, flags: mergedFlags };
178+
179+
// Wrap func to intercept logging flags, capture telemetry, then call original
180+
// biome-ignore lint/suspicious/noExplicitAny: Stricli's CommandFunction type is complex
181+
const wrappedFunc = function (this: CONTEXT, flags: any, ...args: any[]) {
182+
// Apply logging side-effects from whichever flags are present.
183+
// The command's own --verbose (if any) also triggers debug-level logging.
184+
const logLevel = flags[LOG_LEVEL_KEY] as LogLevelName | undefined;
185+
const verbose = flags.verbose as boolean;
186+
applyLoggingFlags(logLevel, verbose);
187+
188+
// Strip only the flags WE injected — never strip command-owned flags.
189+
// --log-level is always ours. --verbose is only stripped when we injected it.
190+
const cleanFlags: Record<string, unknown> = {};
191+
for (const [key, value] of Object.entries(
192+
flags as Record<string, unknown>
193+
)) {
194+
if (key === LOG_LEVEL_KEY) {
195+
continue;
196+
}
197+
if (key === "verbose" && !commandOwnsVerbose) {
198+
continue;
199+
}
200+
cleanFlags[key] = value;
201+
}
202+
90203
// Capture flag values as telemetry tags
91-
setFlagContext(flags as Record<string, unknown>);
204+
setFlagContext(cleanFlags);
92205

93206
// Capture positional arguments as context
94207
if (args.length > 0) {
95208
setArgsContext(args);
96209
}
97210

98-
// Call the original function with the same context and arguments
99-
return originalFunc.call(this, flags, ...args);
211+
return originalFunc.call(
212+
this,
213+
cleanFlags as FLAGS,
214+
...(args as unknown as ARGS)
215+
);
100216
} as typeof originalFunc;
101217

102-
// Build the command with the wrapped function
218+
// Build the command with the wrapped function via Stricli
103219
return stricliCommand({
104220
...builderArgs,
221+
parameters: mergedParams,
105222
func: wrappedFunc,
106223
// biome-ignore lint/suspicious/noExplicitAny: Stricli types are complex unions
107224
} as any);

0 commit comments

Comments
 (0)