Show plugin install counts in the store - #2282
Open
andrewkchan wants to merge 6 commits into
Open
Conversation
Install counts arrive as a document published beside a marketplace's manifest, and they must survive a restart and an offline start the same way the last-known-good catalog does. Give the marketplace row somewhere to keep one. The column is nullable and, on the upsert input, required rather than defaulted: a refresh that did not re-read the sidecar keeps its counts by passing the value it already had, and making that explicit is what forces every writer to say which it means. All three writers say so here and nothing yet fetches a sidecar, so this commit changes no behavior. The migrate test drops the column before replaying a rewind, matching how every other ALTER TABLE ADD in that suite is handled: the ADD is not re-appliable against a table that already has the column. > AGENT GENERATED
Generated by drizzle-kit alongside the migration in the previous commit, and split out because it is 3,739 lines of machine-written JSON that would otherwise bury the whole change. It has to be committed even though nothing reads it at runtime. drizzle-kit generates each migration by diffing schema.ts against the newest snapshot in meta/. Without this file the next person to run db:generate diffs against 0106, does not see stats_json, and re-emits the same ALTER TABLE ADD inside their own migration — which then fails on every database that already applied 0107. All 109 snapshots are tracked for this reason. Reviewers can skip this commit. It is regenerated, never hand-edited. > AGENT GENERATED
A self-contained module, unused until the next commit wires it in. Three decisions live here. The fetch is unconditional rather than replaying the manifest's ETag: the counts move while the manifest sits unchanged behind a 304, which is the whole reason they are a separate document instead of a manifest field. A missing sidecar answers null, because a marketplace need not publish counts at all. And the schema is deliberately not strict, unlike the manifest's: that one is a security contract where an unknown field must reject the document, while this is display metadata a later publisher may extend, and losing every count over an unknown field is worse than ignoring the field. A malformed document is still rejected whole — half-parsed counts are worse than none. Ids outside the manifest's own id pattern are dropped rather than stored: they can never match an entry. > AGENT GENERATED
Wires the sidecar into the catalog and puts the number on the wire. Every refresh of the curated marketplace now fetches the sidecar and stores it in the same transaction as the catalog snapshot, so counts and entries are always published together. A failure warns and keeps the counts already stored: a cosmetic number must never fail a catalog refresh, exactly as a failed manifest read keeps the last-known-good catalog. Only the curated marketplace is asked for a sidecar. BB measures these counts from its own telemetry, so a number beside a third-party listing would be that publisher's claim wearing BB's label — those entries report null and BB never requests the file. Bundled plugins do get counts: telemetry sends a plugin_id for them too, and they are listed under the curated marketplace, so they read from the same document. `installs` joins the catalog search result as nullable with a null default, so a server from before the field degrades to no count rather than to zero — the same shape repositoryUrl already uses. Zero would be a claim; null is the absence of one. Six tests, each failing before this commit: counts on curated and bundled entries, an unnamed entry staying uncounted, the sidecar being re-read while the manifest answers 304, a failed sidecar keeping stored counts while the refresh still records success, a malformed document rejected whole, and a third-party marketplace whose sidecar is never requested. > AGENT GENERATED
Renders the number the server now reports. The store card puts a compact "4.2K installs" in its footer beside the publisher and repository link, with the exact number in the title attribute: a card is read at a glance, and the precise figure is there for anyone who wants it. Mobile appends the same compact label to the browse subtitle. `bb plugin search` prints an exact, comma-grouped number, because a terminal column is read to be compared, and the column appears only once some result carries a count so it stays out of the way otherwise. An entry with no count renders nothing at all rather than a zero, on every surface. > AGENT GENERATED
The plan doc gets the format and the reasoning: why a sidecar rather than a manifest field, why its parser is lenient where the manifest's is strict, why only the curated marketplace publishes one, and that the count undercounts because telemetry is opt-out and production-only. The guide chapter and the bb-cli skill get the user-facing half, since `bb plugin search` grew a column. Both say plainly that the number is what BB heard about, not a true total. > AGENT GENERATED
andrewkchan
force-pushed
the
plugin-install-counts
branch
from
August 22, 2026 07:36
eaf5db7 to
a9d96dd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The plugin store had no popularity signal. Every listing looked equally used, so nothing distinguished a widely adopted plugin from one nobody installs. The data already existed — servers have sent a
plugin_installedtelemetry event (apps/server/src/services/system/telemetry.ts) since the store shipped, carrying aplugin_idfor bundled plugins andbb-communityentries — but nothing ever read it back.The curated marketplace now publishes those counts as a
stats.jsonsidecar beside its manifest (get-bb/marketplace#98). This PR consumes it.What changed
Six commits, each typechecking on its own and meant to be read in order.
1.
db: add a stats_json column to plugin_marketplacesThe column, nullable, plus migration
0107.statsJsonis required onUpsertPluginMarketplaceInputrather than defaulted, so a refresh that did not re-read the sidecar keeps its counts by explicitly passing the value it had. All three writers are updated to say which they mean, and nothing fetches a sidecar yet — no behavior change. The migrate test drops the column before replaying a rewind, matching how every otherALTER TABLE ADDin that suite is handled.2.
db: record the drizzle snapshot for 0107Skippable — 3,739 lines of generated JSON, split out so it does not bury the rest. It has to be committed even though nothing reads it at runtime: drizzle-kit generates each migration by diffing
schema.tsagainst the newest snapshot inmeta/. Without it, the next person to rundb:generatediffs against 0106, does not seestats_json, and re-emits the same ALTER inside their own migration — which then fails on every database that already applied 0107. All 109 snapshots are tracked for this reason.3.
server: parse and fetch the marketplace install-count sidecarA self-contained module, unused until commit 4. Three decisions live here: the fetch is unconditional rather than replaying the manifest's ETag, because the counts move while the manifest sits unchanged behind a 304 — that is the whole reason they are a separate document rather than a manifest field, which the strict manifest schema would reject on an older desktop anyway. A missing sidecar answers null. And the schema is deliberately not strict, unlike the manifest's: that one is a security contract where an unknown field must reject the document, while this is display metadata a later publisher may extend. A malformed document is still rejected whole, because half-parsed counts are worse than none.
4.
server: read install counts on refresh and report them in searchThe wiring, and the only commit that changes what a client sees. Counts are stored in the same transaction as the catalog snapshot, so entries and counts always publish together; a failed fetch warns and keeps the stored counts, since a cosmetic number must never fail a catalog refresh. Only the curated marketplace is asked for a sidecar — BB measures these from its own telemetry, so a number beside a third-party listing would be that publisher's claim wearing BB's label. Bundled plugins do get counts, from the same document.
installsjoins the search result as nullable with a null default, so an older server degrades to no count rather than to zero: zero would be a claim, null is the absence of one.5.
app, mobile, cli: show install countsStore card footer (compact
4.2K installs, exact number in thetitle), mobile browse subtitle, and anInstallscolumn inbb plugin search— exact and comma-grouped there, since a terminal column is read to be compared, and present only once some result carries a count. An uncounted entry renders nothing at all rather than a zero, on every surface.6.
docs: document the install-count sidecarPlan doc gets the format and the reasoning; the guide chapter and bb-cli skill get the user-facing half, since
bb plugin searchgrew a column.No
HOST_DAEMON_PROTOCOL_VERSIONbump: nothing here crosses the server/daemon boundary.apps/webneeded no change — the R2 route already serves any key under/marketplace/v1/and gives.jsonthe revalidating cache-control.Caveat worth stating in review: telemetry is opt-out and only production builds report, so the number is installs BB heard about, not a true total. The docs say so; the UI just shows the number.
How you verified
Six new server tests, all failing before commit 4:
stats.jsonis never requestedPlus a store-card test (compact label, exact
title, singular "1 install", no count for the third-party card, footer does not collapse into a stray separator) and a CLI test (column absent without counts, present with them).Commands:
pnpm exec turbo run typecheckclean across all 75 tasks at each of the six commits individually.testgreen for@bb/server(1928),@bb/db(406),@bb/cli(463),@bb/mobile(839),@bb/server-contract(58),@bb/app.Also fed the registry's real generated output through
parseMarketplaceStatsJsonin a scratch test, to confirm the two repos agree on the format.Pre-existing failures, unrelated and present on a clean tree:
PluginIcon.test.tsx(a local untrackedplugins/directory without apackage.json) and occasional 5s timeouts inFilePreview/update-resolver/timeline-in-turn-windowunder concurrent full-suite load — each passes in isolation.Fixes #