Skip to content

fix(telemetry): suppress first-run notice in --json mode - #1609

Open
clay-good wants to merge 3 commits into
Fission-AI:mainfrom
clay-good:fix/suppress-telemetry-notice-in-json
Open

fix(telemetry): suppress first-run notice in --json mode#1609
clay-good wants to merge 3 commits into
Fission-AI:mainfrom
clay-good:fix/suppress-telemetry-notice-in-json

Conversation

@clay-good

@clay-good clay-good commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Status

LGTM — merge-ready. Full regression gate is clean for this delta (build, lint, tsc --noEmit, openspec validate --strict, changeset, and the full test suite all pass; the fix adds two new passing tests and zero new failures).

Credit

This picks up the last un-shipped sliver of #742 by @CosticaPuntaru ("suppress UI spinners and telemetry notices when --json is used"). Full credit to them for identifying the JSON-cleanliness problem and the telemetry-notice piece specifically. Most of #742 has since shipped independently, so rather than force-push over their branch this is a fresh, minimal PR for the one remaining gap. See the "History" section below.

What was missing

openspec <cmd> --json is meant to emit exactly one machine-readable JSON document on stdout so agents and automation can parse it. Spinner suppression and structured JSON errors already ship on main — but one stdout writer remained: the first-run telemetry disclosure notice.

On a user's first-ever command, maybeShowTelemetryNotice() runs from the global preAction hook and console.logs the disclosure to stdout, before the command's JSON payload. A --json consumer parsing that first run gets invalid JSON. It's first-run-only (the notice then sets noticeSeen), but that's exactly the run an automation hits on a fresh machine or CI image.

What it does

  • maybeShowTelemetryNotice() takes a silent option. When silent it prints nothing and leaves noticeSeen unset — so the disclosure is deferred, not skipped.
  • The preAction hook suppresses the notice whenever --json appears in the invocation, detected from process.argv.

Why argv, not a parsed option: --json reaches commands three ways, and a single parsed option (actionCommand.opts().json) misses two of them:

  • leaf option — openspec status --json ✓ (a parsed check would catch this one)
  • parent group read via optsWithGlobalsopenspec workset --json list (leaf's opts().json is undefined)
  • residual arg on a permissive group that never declares the option — openspec store --json (emits a single JSON document via raw-arg detection)

Detecting --json from argv covers all three uniformly. It's the conservative choice: suppressing is always safe (worst case the disclosure defers one run — never lost), while printing the notice on a JSON run corrupts stdout.

Net effect: any --json invocation never emits the notice on stdout; the user still sees the disclosure on their first later non-JSON run. Telemetry stays opt-out and otherwise unchanged; no new data is collected.

Out of scope: a few commands write scriptable output to stdout without a --json flag (completion generate, config get, config path, the hidden __complete). Their first-run notice pollution is a separate, pre-existing issue not addressed here.

This also incorporates the maintainer review feedback left on #742: the noticeSeen-persisted-while-silent bug is fixed (deferral), and the spec is a proper telemetry MODIFIED delta rather than a new machine-readable-output capability.

Proof it works

  • New unit tests in test/telemetry/index.test.ts: first-run --json (silent) prints nothing and leaves noticeSeen unset; the disclosure still appears on the first later non---json run.
  • New test/core/cli-is-json-run.test.ts: a synthetic Commander program reproducing all three registration patterns proves isJsonRun returns true for status --json, store --json, workset --json list, and workset list --json, and false otherwise — a regression guard for the store/workset coverage (an e2e test can't guard it: telemetry is disabled under CI, so the notice never fires there).
  • End-to-end (fresh config, telemetry on, network blocked): first-ever run of status --json, store --json, workset --json list, and workset list --json each produce clean, valid JSON on stdout with zero notice lines; a first-ever non-JSON status shows the notice (deferral intact).
  • openspec validate suppress-telemetry-notice-in-json --strict passes.
  • Full suite: 2793 passing; the 3 files that fail (artifact-workflow, config-profile, command-generation/adapters) fail identically on pristine main — pre-existing and unrelated to this change.

History / why this is small

The bulk of #742 shipped independently after it was opened:

So #957 is already resolved; this PR closes no issue. The only genuinely-remaining behavior from #742 is the telemetry-notice guard above.

Related (not closed): #1526 (spinner ANSI to non-TTY stdout) is the same failure family for archive, handled separately by #1603.

Summary by CodeRabbit

  • Bug Fixes

    • JSON-mode commands no longer include the first-run telemetry notice in machine-readable output.
    • The notice remains undisplayed during JSON runs and appears on the first subsequent non-JSON command.
    • Non-JSON telemetry notice behavior remains unchanged.
    • JSON mode is recognized consistently across supported command formats.
  • Tests

    • Added coverage for notice persistence, repeated runs, JSON-mode suppression, and subsequent disclosure.

The first-run telemetry disclosure notice was written to stdout from the
global preAction hook. On a user's first-ever command with --json this
polluted stdout and could break JSON parsers. Read the executing command's
--json flag (actionCommand.opts().json) and, when set, skip the notice and
leave noticeSeen unset so the disclosure is deferred to the first later
non-JSON run rather than lost.

Spinner suppression, new-change --json output, and structured JSON errors
already landed on main (Fission-AI#960, Fission-AI#1190); this closes the one remaining stdout
writer in --json mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@clay-good
clay-good requested a review from a team as a code owner August 7, 2026 21:08
@clay-good
clay-good requested review from TabishB and removed request for a team August 7, 2026 21:08
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI now suppresses the first-run telemetry notice during --json commands. The notice remains unseen and appears on the first later non-JSON command. Tests and OpenSpec documentation define and verify this behavior.

Changes

Telemetry notice suppression

Layer / File(s) Summary
Notice behavior contract
openspec/changes/suppress-telemetry-notice-in-json/...
Defines silent notice handling, JSON propagation, and deferred disclosure requirements.
Telemetry notice state handling
src/telemetry/index.ts, test/telemetry/index.test.ts
Adds the optional silent setting. Silent runs omit output and preserve the unset noticeSeen state. Tests cover repeated and deferred disclosure.
JSON command wiring and release metadata
src/cli/index.ts, test/core/cli-is-json-run.test.ts, .changeset/suppress-telemetry-notice-json.md
Detects JSON mode across command options and residual arguments, passes silent: true, tests detection, and records the patch release change.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Telemetry
  participant Config
  CLI->>CLI: Detect --json from command options or residual arguments
  CLI->>Telemetry: Call maybeShowTelemetryNotice({silent: true})
  Telemetry->>Config: Keep noticeSeen unset
  CLI->>Telemetry: Call maybeShowTelemetryNotice() on later non-JSON run
  Telemetry->>Config: Persist noticeSeen after displaying the notice
Loading

Possibly related PRs

Suggested reviewers: tabishb, alfred-openspec

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: suppressing the first-run telemetry notice during --json execution.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/telemetry/index.test.ts (1)

195-208: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a CLI-level regression test for the preAction wiring.

These tests call maybeShowTelemetryNotice() directly. They do not verify that an actual --json command sets actionCommand.opts().json or that the complete stdout is one parseable JSON document. Add a fresh-process CLI test for a first --json run, followed by a non-JSON run that verifies deferred disclosure.

As per coding guidelines, run the focused test with pnpm exec vitest run test/telemetry/index.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/telemetry/index.test.ts` around lines 195 - 208, Add a fresh-process CLI
regression test covering the preAction wiring: execute a first command with
--json, verify stdout contains only one parseable JSON document and no telemetry
notice, then execute a non-JSON command and verify the deferred “OpenSpec
collects anonymous usage stats” disclosure appears. Keep the existing direct
maybeShowTelemetryNotice test unchanged and run the focused test with pnpm exec
vitest run test/telemetry/index.test.ts.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openspec/changes/suppress-telemetry-notice-in-json/specs/telemetry/spec.md`:
- Around line 6-9: Qualify the “First command execution” scenario in the
telemetry specification as applying only to non-JSON commands, either by
renaming it to “First non-JSON command execution” or adding an explicit
condition that the command does not pass --json; preserve the existing
telemetry-enabled notice behavior.

---

Nitpick comments:
In `@test/telemetry/index.test.ts`:
- Around line 195-208: Add a fresh-process CLI regression test covering the
preAction wiring: execute a first command with --json, verify stdout contains
only one parseable JSON document and no telemetry notice, then execute a
non-JSON command and verify the deferred “OpenSpec collects anonymous usage
stats” disclosure appears. Keep the existing direct maybeShowTelemetryNotice
test unchanged and run the focused test with pnpm exec vitest run
test/telemetry/index.test.ts.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c1fda48d-c821-42f2-964c-075d0d6d0ae2

📥 Commits

Reviewing files that changed from the base of the PR and between e50bd09 and 77d41eb.

📒 Files selected for processing (8)
  • .changeset/suppress-telemetry-notice-json.md
  • openspec/changes/suppress-telemetry-notice-in-json/.openspec.yaml
  • openspec/changes/suppress-telemetry-notice-in-json/proposal.md
  • openspec/changes/suppress-telemetry-notice-in-json/specs/telemetry/spec.md
  • openspec/changes/suppress-telemetry-notice-in-json/tasks.md
  • src/cli/index.ts
  • src/telemetry/index.ts
  • test/telemetry/index.test.ts

Comment on lines +6 to +9
#### Scenario: First command execution
- **WHEN** a user runs their first openspec command
- **AND** telemetry is enabled
- **THEN** the system displays: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Qualify the generic first-run scenario as non-JSON.

Lines 7-9 apply to every first command, but Lines 20-23 define no notice for a first --json command. The specification therefore contains contradictory acceptance criteria. Rename this scenario to First non-JSON command execution or add AND the command does not pass --json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/suppress-telemetry-notice-in-json/specs/telemetry/spec.md`
around lines 6 - 9, Qualify the “First command execution” scenario in the
telemetry specification as applying only to non-JSON commands, either by
renaming it to “First non-JSON command execution” or adding an explicit
condition that the command does not pass --json; preserve the existing
telemetry-enabled notice behavior.

clay-good and others added 2 commits August 7, 2026 16:27
The preAction guard read actionCommand.opts().json, which only sees a
declared leaf option. That missed two supported --json forms that emit a
single JSON document to stdout:
  - openspec store --json  (permissive group reads --json from residual args;
    never declares the option, so opts().json is undefined)
  - openspec workset --json <sub>  (--json on the parent group, consumed
    before the leaf; leaf opts().json is undefined)
Both would still print the first-run telemetry notice ahead of their JSON.

Detect --json from process.argv instead: it covers leaf, parent, and
residual-arg forms uniformly. Suppressing is always safe (the disclosure
defers to the next non-JSON run, never lost), so a broad argv check is the
correct, conservative signal.

Also add a direct assertion that noticeSeen stays unset after a silent run,
and note the pre-existing raw-stdout commands (completion generate, config
get/path, __complete) as out of scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the process.argv check with isJsonRun(command), an exported pure
helper that reads Commander's parsed state: optsWithGlobals().json (leaf and
parent-group forms) OR command.args (residual --json on permissive bare
groups like store). This is tied to the actually-parsed command rather than
raw args, and — unlike process.argv — is unit-testable in-process.

Add test/core/cli-is-json-run.test.ts: a synthetic program reproducing all
three registration patterns proves isJsonRun returns true for status --json,
store --json, workset --json list, and workset list --json, and false
otherwise. This locks in the store/workset coverage against future
regressions (an e2e test can't: telemetry is disabled under CI, so the notice
never fires there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/index.ts`:
- Around line 132-136: Update isJsonRun so --json is recognized only when parsed
as an option, not when present in command.args after the -- terminator. Preserve
the parsed option state and add a regression covering ['store', '--', '--json']
that expects false.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6eeed1b7-0835-4511-a47d-333252bd9e6d

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4c119 and 6164504.

📒 Files selected for processing (3)
  • openspec/changes/suppress-telemetry-notice-in-json/proposal.md
  • src/cli/index.ts
  • test/core/cli-is-json-run.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • openspec/changes/suppress-telemetry-notice-in-json/proposal.md

Comment thread src/cli/index.ts
Comment on lines +132 to +136
export function isJsonRun(command: Command): boolean {
return (
command.optsWithGlobals().json === true ||
command.args.includes('--json')
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'allowUnknownOption|allowExcessArguments|--json|isJsonRun' src test

Repository: Fission-AI/OpenSpec

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the CLI helper and surrounding tests, and list dependency versions/config snippets.
sed -n '120,170p' src/cli/index.ts
printf '\n--- relevant telemetry helper/tests ---\n'
sed -n '1,230p' test/telemetry/index.test.ts
printf '\n--- package commander version refs ---\n'
rg -n '"commander"|commander' package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null | head -80

Repository: Fission-AI/OpenSpec

Length of output: 9706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate definitions and usages that make subcommands accept residual --json.
rg -n "allowUnknownOption|allowExcessArguments|command\.args|function createProgram|program|subcommand|hooks" src test/telemetry/index.test.ts | head -200
printf '\n--- candidate source files ---\n'
git ls-files src | rg 'cli|command|index' | head -100

Repository: Fission-AI/OpenSpec

Length of output: 15953


🌐 Web query:

Commander.js terminal option terminator -- options after stop option processing args

💡 Result:

In Commander.js, the double dash (--) serves as a terminal option terminator [1][2][3]. When encountered, it instructs the parser to immediately stop processing further command-line arguments as options [2][4][3]. Any remaining arguments provided after the -- are treated strictly as positional operands (or command-arguments) and are not interpreted or parsed as flags, even if they begin with a dash (-) [1][5][6]. This behavior aligns with the standard POSIX guideline (Guideline 10), which uses -- as a delimiter to signify the end of options [6]. Common use cases include: 1. Passing arguments that start with a dash to a subcommand or script, ensuring they are not mistakenly parsed as options for your program [6]. 2. Separating your program's own flags from external arguments that might conflict with them [1]. If you want to avoid requiring your users to use --, you have a few alternatives: - Put your options before any positional arguments [1]. - Use.passThroughOptions to stop processing options automatically when a non-option argument is encountered, allowing subsequent arguments (including options meant for another program) to be passed through unprocessed [2][7][3]. - Incorporate -- into your documented usage syntax if you want to make it an explicit part of your command interface [1].

Citations:


🌐 Web query:

Commander.js v14 allowExcessArguments parse behavior command.args after --

💡 Result:

In Commander.js v14, the behavior of command-line arguments is defined by the default setting of.allowExcessArguments(false) [1][2]. 1. Excess Arguments Behavior Since version 13.0.0, Commander throws an error by default if a user provides more arguments than the command expects [2][3]. If your application receives unexpected arguments, the parser will stop and display an error message (e.g., "error: too many arguments") [1][4]. To revert to legacy behavior where excess arguments are silently ignored or collected in command.args, you must explicitly call.allowExcessArguments on your program or command instance [1][3]. 2. Parsing with -- The -- separator is a standard feature in Commander.js that signals the end of option processing [5][6]. Any arguments appearing after -- are treated as literal arguments, not as options [5][7]. Crucially, when -- is used: - Arguments following -- are still subject to the.allowExcessArguments check [4]. If you have a command that expects specific arguments and you provide more than that total (including those after --), Commander will still throw a "too many arguments" error unless.allowExcessArguments(true) is configured [4]. - These arguments are typically captured in command.args if they do not violate the excess argument limit [5][6]. - If you intend to pass arbitrary arguments through to a subprocess or another utility, using -- effectively stops the current command's option parser from misinterpreting flags that follow [8][6]. If you find yourself needing to manually parse or trim these arguments, consider using.passThroughOptions, which can sometimes obviate the need for -- by passing all subsequent arguments through to the action handler or a subcommand [9][6].

Citations:


Do not treat a post-terminator operand as JSON mode.

command.args includes arguments after --, and Commander stops option parsing at --. An invocation such as openspec store -- --json will match command.args.includes('--json') and suppress the telemetry notice as JSON output, even though --json is an operand, not an option. Preserve whether --json was parsed as an option and add a regression for ['store', '--', '--json'] that expects false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/index.ts` around lines 132 - 136, Update isJsonRun so --json is
recognized only when parsed as an option, not when present in command.args after
the -- terminator. Preserve the parsed option state and add a regression
covering ['store', '--', '--json'] that expects false.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant