Withdraw the account-wide boosts feed - #427
Conversation
GET /boosts.json is withdrawn while we rework how it queries at scale. The rest of the Everything feeds and the project-scoped boosts endpoints are unchanged.
There was a problem hiding this comment.
🟢 Ready to approve
The withdrawn endpoint is fully removed from the Everything docs without leaving broken references, and the remaining feeds are unchanged.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Withdraws the account-wide Everything boosts feed (GET /boosts.json) from the Basecamp API documentation due to scalability concerns, while keeping the rest of the Everything feeds and project-scoped boosts documentation intact.
Changes:
- Removed
/boosts.jsonfrom the list of account-wide Everything root collections. - Removed the “Boosts / Get boosts” endpoint section (including example response and cURL) from
sections/everything.md. - Cleaned up related endpoint/link references in the Everything section.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
| sections/everything.md | Removes the account-wide /boosts.json feed documentation from the Everything endpoints section. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Low
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
BC3's brief supersedes the guesswork in the previous commit on two points, both worth having in the tree rather than only in an issue thread. The cause is not the derived-table materialization this repo inferred: the query's cost is proportional to the account's accessible recordings rather than to its boosts. That explains the ~44s and the flat cost curve without needing the EXPLAIN we asked for. And the withdrawal is temporary. The feed returns on a boost-proportional query via a boosts.bucket_id denormalization (basecamp/bc3#12463); the withdrawal itself is basecamp/bc3#12464, with public notice at basecamp/bc-api#427. So the docs now describe a listing that is gone for now, not one judged a bad idea — and boost.go says where the account-wide branch goes back when it returns. The SDK keeps Everything().Boosts() deliberately: removing a generated operation for a temporary withdrawal would churn every generated client twice. It 404s in the interim; the CLI simply does not call it. e2e/boost.bats needed no change — it covers help only, with no account-wide case to remove.
* Bound the account-wide listings instead of crawling the account Soaking v0.8.0-rc.1 against a ~80-project account showed the account-wide listings are unusable at real scale. The cause is a contract decision, not a bug: I5 pinned each command's account-wide default to its project-scoped default. But project-scoped "all" is one project's items and account-wide "all" is the whole account, so the same word bought very different work. files list --all-projects HTTP 500 mid-crawl, no flag to recover with cards list --all-projects --status completed timed out at 94s (--page 1: 24s) checkins answers --all-projects timed out (one page: 1s) I5 also blessed "fetch page 0, then truncate", which made --limit useless exactly when it mattered: cards --limit 3 fetched all 2142 cards before trimming. Add accountWideCollect, one bounded walk over positive pages shared by all eight paths. It stops on an empty page, a page that adds no items, the cap, or the server's total. Two rules callers must know, so they are stated on the helper: Meta.TotalCount compares against len(items) — the top-level elements, which for a grouped payload are the groups, not the items nested inside — and the walk does not trim, so every caller cuts to the exact cap itself. Default to 100 for the paginated listings; --all is how you ask for the account. This changes the default for cards, checkins, forwards, and files. Project-scoped defaults are untouched. Give boost list and bare files list the --limit/--page/--all they never had. A listing with no escape hatch cannot recover from a server error mid-crawl, which is exactly how the files 500 became unrecoverable. Where a path has no pagination to thread — project-scoped files list, item-scoped boost list — it rejects all three by name rather than accepting and ignoring them. Both overdue paths now accept --all and reject --page. They are unpaginated, so --page addresses nothing, but capping at 100 while also rejecting --all left item 101 unreachable by any combination of flags. boost keeps a first-page default: /boosts.json?page=1 alone measured 93s, so walking toward 100 by default would multiply an already unacceptable wait. The exemption covers the default only — its flags walk like everywhere else — and its notices are per-mode, since "Showing first page" is false for --page 2. The slowness is server-side and tracked there. Dropping accountWideRespOpts's moreFlag parameter falls out of this: it existed so the two flagless commands would not be told to use an --all they did not have. They have one now. * Record the measured cause of the boost feed's latency The contract doc carried a hypothesis; production tracing settles it. Server timings for one page=40 request, 44,630ms total: Boost Load 22,097.6ms the paginated SELECT ~60 preloads <2.3ms each Boost Count 22,265.7ms GearedPagination's total-count query Completed 200 OK in 44630ms (Views: 244.7ms | ActiveRecord: 44370.9ms) 99.5% database time, in two statements of near-identical cost. That corrects the record twice over: the N+1 hypothesis is dead — the controller's preloads work, and 60 of 64 queries are trivial — and ordering the derived table, which the doc named as the suspect, is only half the story. The count pays the same materialization again for pagination metadata, so fixing the ORDER BY alone would leave roughly half the latency behind. The 93s figure was also wrong: that was the client giving up after three 30s attempts. A page costs ~44s, and page 40 costs the same as page 1 — the flat cost curve the materialization predicts. Since ~44s exceeds the SDK's 30s client timeout, boost list --all-projects does not merely run slow, it cannot complete. Filed as basecamp/bc3#12458, tracked alongside the files.json 500 (basecamp/bc3#12459) in basecamp-cli#589. * Stop reporting a listing that ends at the cap as truncated accountWideCollect checked the cap before exhaustion, so a listing whose total equalled the limit came back capped=true and the callers turned that into "more may exist" about a listing with nothing left in it. Test the known total first, and let the trim decide: holding every item still counts as capped when the caller is about to cut down to the cap. The exhaustion bound also read TotalCount from the current page while the helper documents the first page's as authoritative. A later page omitting X-Total-Count switched the bound off mid-walk and kept requesting past the declared end of the listing. messages and comments were discarding capped entirely, which made truncation silent on exactly the feeds that withhold a total — the case the notice exists for. messages also had a local `capped` meaning "this invocation applies a cap", which is a different question from "the walk stopped short"; it is now `bounded`, and the sorted path sets capped when its own trim removes anything. * Separate what the boost tracing measured from what it infers The doc claimed the derived-table materialization as established. The tracing established the shape — two ~22s statements, 99.5% db time, N+1 disproved — but not the query plan behind it, and bc3#12458 correctly asks for an EXPLAIN rather than asserting one. Say the same thing here. * Drop the account-wide boost listing rather than page around it The /boosts.json aggregate is an easter egg: unlinked from the Basecamp web UI, and served by a query that spends ~44s in MySQL on every page regardless of depth (basecamp/bc3#12458). It is being withdrawn server-side, so the CLI stops calling it instead of building a contract on top of that. boost list now requires an item ID. It loses --all-projects along with the --limit/--page/--all this branch had given it: an item's boosts arrive in one unpaginated response, and the SDK documents BoostListOptions.Page as not honoring a page number, so there was never anything for those flags to address once the aggregate went away. rejectScopedPaginationFlags is no longer needed here — flags that do not exist cannot be silently ignored. Everything().Boosts() stays in the SDK. Whether it should is the SDK's call; this repo just stops reaching for it, and API-COVERAGE.md now records 16 of the 17 aggregates as surfaced with the reason for the sixteenth. Seven commands list account-wide, not eight. On the surface baseline: --limit/--page/--all on boost were added and removed inside this unmerged branch, so they never shipped and recording them in .surface-breaking would claim a break that never happened. They are rewound out of the baseline instead. The two changes that did ship in v0.8.0-rc.1 -- `--all-projects`, and the argument going from optional to required -- are acknowledged as real removals. Net surface change against rc.1 is exactly those two. * Record that the boosts withdrawal is temporary, and BC3's own cause BC3's brief supersedes the guesswork in the previous commit on two points, both worth having in the tree rather than only in an issue thread. The cause is not the derived-table materialization this repo inferred: the query's cost is proportional to the account's accessible recordings rather than to its boosts. That explains the ~44s and the flat cost curve without needing the EXPLAIN we asked for. And the withdrawal is temporary. The feed returns on a boost-proportional query via a boosts.bucket_id denormalization (basecamp/bc3#12463); the withdrawal itself is basecamp/bc3#12464, with public notice at basecamp/bc-api#427. So the docs now describe a listing that is gone for now, not one judged a bad idea — and boost.go says where the account-wide branch goes back when it returns. The SDK keeps Everything().Boosts() deliberately: removing a generated operation for a temporary withdrawal would churn every generated client twice. It 404s in the interim; the CLI simply does not call it. e2e/boost.bats needed no change — it covers help only, with no account-wide case to remove. * The SDK is dropping the boosts aggregate too, not just the CLI The withdrawal brief asked that Everything().Boosts() stay in the SDK, on the grounds that churning every generated client twice for a temporary withdrawal was worse than letting it 404. That call has changed: the endpoint is going away for real server-side, so shipping an operation that cannot work is worse than the churn. Nothing to do here beyond correcting the docs, which said the opposite. The CLI carries zero references to the aggregate — verified across code and tests, not just assumed — so whichever SDK release removes it lands as a no-op, and the next bump needs no CLI change. API-COVERAGE.md records that its "16 of 17" becomes 16 of 16 once the CLI pins an SDK without it, rather than pre-claiming a version that isn't pinned yet. Also drops a stale I6 line about EverythingBoost's pointer fields: nothing flattens that payload any more. * Stop the bounded walk dropping every page after a sparse one Five review findings, two of them correctness. A grouped page whose groups all happen to be empty is sparse, not terminal, and the walk returned on it — silently dropping every project on every later page, and suppressing the truncation notice while doing so. My own test enshrined the data loss, asserting page 3 was never requested. It now asserts the opposite. The justification for that guard was wrong on its face: it claimed to prevent requesting "the same page shape forever", but the loop increments page, so it was never at risk of re-requesting anything. The empty page is the server's actual end-of-listing signal and is now the only content-based stop. A page ceiling replaces the guard as the runaway backstop, and hitting it reports capped rather than passing a bounded walk off as a complete listing. The collected slice started nil, so an empty first page marshalled to "data": null while the SDK-backed --all path returns []. Consumers iterating .data[] had to handle both. Verified against production: an account-wide listing with no matches now returns []. Three contract defects alongside them. cards, checkins and forwards still advertised "--limit 0 = all" while their account-wide default had become 100. accountWideRespOpts told users to "drop --limit for more", which returns fewer once the explicit limit exceeds the default. And accountWideCapNotice claimed to defer whenever the server reported a total but only deferred when that total exceeded the count, so it could contradict the server by saying "more may exist" when the server said there was nothing more. * Stop telling agents to use --page on the overdue listings SKILL.md said every account-wide listing takes --page. The two overdue variants come from unpaginated endpoints and reject it, so an agent following the skill would generate a command the CLI refuses. Scope --page to the paginated listings and name the exception, with what the overdue paths do accept: --limit and --all. Verified against production rather than read off the source: both overdue variants reject --page 2 and return rows for --all. Note the drift checker cannot catch this class of error. It validates that a flag exists on a command, and --page does exist on todos list and cards list; it is rejected at runtime only in combination with --overdue. The prose is the only guard, which is the reason to keep it exact.
The account-wide boosts feed,
GET /boosts.json, is withdrawn. The endpoint's current query doesn't hold up on our largest accounts, so we're pulling it while we rework how it fetches boosts at scale. We expect to bring it back.Everything else is unchanged: the other Everything feeds (
/messages.json,/comments.json,/checkins.json,/forwards.json,/files.json, and the to-do/card filters), the project-scoped boosts endpoints underbuckets/…/recordings/…/boosts.json, and theboosts_count/boosts_urlattributes on recording payloads all remain as documented.Synced from bc3
doc/api/byscript/api/sync_to_bc3_api— not a hand-edit.