fix(telemetry): global-only consent and endpoint, isolated test delivery, test-only Sentry issues no longer block releases
Summary
This PR fixes two problems with test telemetry.
First, the release Sentry gate counted issues raised only by test runs, so a downstream extension's CI blocked the daily release (pm-usn5k2).
Second, every project-level telemetry setting was inert, which made several consent paths ineffective (pm-gh1660 / GH-1660). The telemetry runtime reads consent and the event endpoint only from the global profile. On 2026.11.4, these four surfaces wrote project values that nothing read:
- Setup wizard:
pm init's "Enable telemetry for this project? [Y/n]" answer went only into the projectsettings.json. I reproduced this in a disposable profile: after answering n, status still reportedenabled: trueand later commands delivered events. - Documented opt-out:
pm config set telemetry-tracking offfrom docs/CONFIGURATION.md resolves to project scope when run inside a tracker. It reportedchanged: truewhile telemetry kept running. - Project-scope config:
pm config project set telemetry-trackingaccepted a value nothing reads. - Project endpoint: the
telemetry.endpointthatpm initwrites into the project file was ignored, and there was no supported way to point opted-in tests at a local collector.
Release Sentry gate (pm-usn5k2)
- The gate query is now
is:unresolved level:[fatal,error] !environment:test, applied to both the API and thesentryCLI fallback. - Sentry drops an issue only when all of its events are
environment=test. An issue with any production event still blocks. I checked this live against the project: 17 issues became 11, and a mixed-environment issue was kept. - A negative-control spec models that search and shows production and mixed issues still blocking.
- docs/RELEASING.md describes the exclusion. docs/CONFIGURATION.md now says that
PM_TELEMETRY_SOURCE_CONTEXT=testopts into delivery, so a test that only wants the label must also setPM_TELEMETRY_DISABLED=1.
Telemetry consent and endpoint live in the global profile (pm-gh1660)
- Wizard consent:
- The answer is written to the global profile.
- The question is asked only while no consent is recorded and no environment opt-out applies, so pressing Enter can never override an earlier global choice.
- The prompt now says the choice applies to this CLI install.
- No project block:
pm initwrites no projecttelemetryblock, and later writes keep that shape. A full settings write never removes a section the stored file already has, so even a path that aliases the global profile keeps its block. telemetry-trackingis global-only:- The scope-less CLI shorthand and a scope-less MCP call target the global profile, so the documented opt-out works.
- Project-scope
getandsetrefuse withinvalid_argument_value(reason: telemetry_consent_is_global) and name the global command. - Project
listandexportomit the key, soConfigResult.valuesis nowPartial.
- Health: health reads telemetry consent and endpoint from the global profile.
PM_TELEMETRY_ENDPOINT for opted-in tests
- Validation: an absolute http(s) URL redirects event delivery. Any other non-empty value disables delivery and is reported in
env_overrides. It never falls back to the configured endpoint. - Isolation: events captured under an override get a queue file and delivery-state file of their own (
events.<key>.jsonl,state.<key>.json).- No flush without the same override reads them, and neither does any older client.
- Each flush resolves the override once, for both the file it reads and the endpoint it posts to. So an environment change between command start and finish cannot route an event to the wrong place.
- Span state and alias usage stay in the shared state file.
- A later flush removes override files that have been idle for a day.
- Reporting: status, the flush receipt, stats and health report the queue the override actually uses. Endpoints shown in diagnostics have credentials, query and fragment removed.
- Docs: the "Test environments" section names
PM_TELEMETRY_ENDPOINTfor events,SENTRY_DSNfor errors andOTEL_EXPORTER_OTLP_*for spans.
Package weight (pm-998juj)
The fix adds 14,441 first-party bytes, which pushes the package over the unchanged 20,000,000-byte ceiling. The bytes are mostly minified code that is emitted twice, once by tsc and once into the bundle, plus declaration JSDoc.
Package staging now runs compactMarkdownTables on the publication copy, next to the existing JSON compaction:
- It collapses GFM table cell padding and separator dash runs.
- It compacts only real GFM tables, never fenced code or raw HTML blocks. It leaves alone any file whose fences sit inside list items, block quotes or indentation, refuses any row whose reparsed cells or alignment would change, and leaves checkout files readable.
A spec renders every tracked markdown file with marked (GFM) before and after compaction and asserts identical HTML. The distribution is now 19,976,257 bytes. The ceiling is unchanged, and no runtime file, type or export is removed.
defaultConfigScope is exported from the SDK (runtime-primitives), so the CLI and MCP hosts share it without a private core import.
Verification
- Review:
- Codex could not run this round: its refresh token expired, and only an interactive
codex loginfixes that. - An independent review agent ran five rounds on the code, without my conclusions. Round 1 found 7 issues: two P1 routing defects, a P1 alias case, a health regression, a coverage gap, endpoint credentials in diagnostics, and docs. Rounds 2 to 4 found 3, 2 and 1 smaller issues. Every one was fixed with a spec. Round 5 found no remaining issues.
- Codex could not run this round: its refresh token expired, and only an interactive
- Negative controls: the alias spec, the queue-isolation spec and the gate spec each fail without their fix.
- End to end: I ran the built CLI in a disposable profile.
- The wizard decline disables consent globally.
- The documented opt-out reports
scope: globalandconfigured_enabled: false. - Six events reached a local listener through
PM_TELEMETRY_ENDPOINT, and an invalid value disables delivery.
- Static chain: the full
pnpm quality:staticchain passes locally after rebasing onto main. - Coverage: the full local coverage run passed all 12,033 tests and found three uncovered branches, two in
config.tsand the markdown branch of the packager. All three are now covered, which I checked with focused v8 coverage. The hosted coverage shards are the full check. - Tracker gates:
tracker-context-quality,graph-composition,record-integrity, the defect-evidence gate,changelog:pm:checkand the secret scan pass. - pm-usn5k2: it stays open, because its pm-vcs criterion lands with unbraind/pm-vcs#179.
Review follow-ups (d2b1426d1)
CodeRabbit raised 7 findings. A follow-up independent review of those fixes raised 8 more. All are fixed with specs:
- Init consent:
WriteSettingsOptions.skipIfStoredis a compare-and-set predicate thatwriteSettingschecks under the settings lock. The wizard's global write uses it, so a consent choice another process records while the prompt is open stands, and init warnstelemetry_consent_kept:recorded_while_prompting. The wizard never asks, and never writes, when the global profile is unreadable. - Init output: the result reports
enabled,first_run_prompt_completedandcapture_levelfrom the global profile. The global endpoint, which may carry credentials, and the installation id never appear in it. - Health probe:
- URL credentials are sent as Basic auth, because
fetchrejects URLs with userinfo. - A malformed escape is sent as written.
- Credentials are redacted from probe URLs and from transport errors, and also from endpoint text that is not a valid URL.
- A malformed shared
state.jsonwarns even under an endpoint override.
- URL credentials are sent as Basic auth, because
- Table compaction:
- HTML start conditions 1 to 7 are honoured.
- Fences close only on bare markers.
- Dash-only body rows keep their text, and rows must share the header's indentation.
- CRLF is kept.
- The render proof lives in the spec, because release controls run the packager without
node_modules.
- Residual: a settings read of a missing
settings.jsoncarries no snapshot, so a later write can replace a profile created in the meantime. I reproduced this against the built store, and it affects the runtime first-use prompt. It is filed as pm-nroemi. - Local verification:
pnpm quality:staticpasses, with a mutation score of 98.01.- 12,042 tests pass.
- Exact coverage is complete, including focused v8 coverage of the packager at 161/161 branches.
- The package-artifact gate and
changelog:pm:checkpass.
- Reviewers: Codex was still unavailable because its token is expired. Sourcery, Greptile and cubic are out of quota or trial.
Summary by Sourcery
Make telemetry consent and test delivery safe and effective while excluding test-only Sentry issues from release blocking and keeping the published package under its size ceiling.
New Features:
- Add PM_TELEMETRY_ENDPOINT support for routing opted-in test events to isolated collectors and queues.
Bug Fixes:
- Make telemetry consent and event endpoints consistently use the global profile so setup, configuration, MCP, health, and diagnostics reflect effective settings.
- Prevent unresolved Sentry issues seen only in test environments from blocking releases while retaining production and mixed-environment issues.
- Protect telemetry diagnostics and delivery state from exposing endpoint credentials, query parameters, or fragments.
Enhancements:
- Isolate endpoint-override event queues and state, preserve shared span and alias state, and clean up abandoned override artifacts.
- Compact Markdown tables in publication artifacts without changing rendered content to keep the package below the existing size limit.
- Export shared configuration-scope resolution through the SDK runtime primitives.
Build:
- Compact publication Markdown tables to reduce package size without modifying checkout files.
Documentation:
- Document global-only telemetry consent, test endpoint overrides, test delivery behavior, and the Sentry release-gate exclusion.
Tests:
- Add coverage for global consent routing, endpoint override isolation and redaction, health behavior, Sentry environment filtering, and lossless Markdown compaction.
Chores:
- Record the related telemetry, release-gate, and package-size issues and follow-up race condition in project tracking.
Summary by cubic
Fixes telemetry so opting out or declining actually stops event delivery, routes opted-in test events to an isolated collector, and stops test-only Sentry issues from blocking the daily release.
Telemetry consent and delivery (pm-gh1660 / GH-1660)
- Consent and the event endpoint are read only from the global profile; the init wizard,
pm config set telemetry-tracking off, and the MCP shorthand target it, and project-scope get/set refuse withinvalid_argument_value. - The wizard's global consent write uses a compare-and-set predicate under the settings lock, so a choice recorded while the prompt is open stands; init never writes a project
telemetryblock. PM_TELEMETRY_ENDPOINTroutes opted-in test events to their own queue and collector; only an absolute http(s) URL delivers, and any other non-empty value disables delivery.- The override is resolved once per flush, idle override files are removed after a day, and diagnostics and stored transport errors redact credentials, query, and fragment from every URL.
- A residual race (pm-nroemi) remains: a settings read of a missing
settings.jsoncarries no snapshot, so the runtime first-use prompt andpm init's whole-document write to the global profile can replace a profile (including a concurrent telemetry opt-out) created meanwhile.
Release gate and package (pm-usn5k2, pm-998juj)
- The Sentry gate drops issues whose events are all
environment=test, judging activity and test-only environments in separate queries because Sentry applieslastSeenafter the environment filter; issues with any production event still block. - The gate follows Sentry's next-page links up to
--sentry-limitfor both listings, and a cut-off active listing fails the gate because issues beyond the limit are unjudged; a truncated non-test listing drops nothing. - Package staging compacts GFM table padding in the publication copy, trimming only spaces and tabs as GFM does, skipping real code and HTML blocks, leaving every tracked markdown file render-identical and the distribution at 19,979,096 bytes under the unchanged 20,000,000-byte ceiling.