Publish install counts as a stats.json sidecar - #98
Merged
Conversation
BB has collected a `plugin_installed` telemetry event since the store shipped, but nothing ever read it back, so the marketplace had no way to show how popular a listing is. Add a daily job that queries those counts and publishes them beside the manifest. `scripts/build-stats.mjs` queries PostHog's HogQL API for `count(DISTINCT distinct_id)` of `plugin_installed` grouped by `properties.plugin_id`, and writes `dist/stats.json`. Distinct installations, not raw events: a reinstall loop must not inflate a listing. Rows whose id fails the manifest's own id pattern, or whose count is not a non-negative safe integer, are dropped with a warning; keys are emitted sorted so an unchanged run republishes identical bytes and keeps its ETag. A run that ends with no usable counts exits non-zero without writing, so an outage or a rotated key leaves the last published sidecar serving instead of zeroing every counter in the store. The counts are a separate document rather than a field in marketplace.json on purpose: that schema is strict, so an unknown field there rejects the whole catalog on an older desktop, and the numbers move daily while the manifest sits unchanged behind a 304. `.github/workflows/stats.yml` runs it daily and uploads to the same R2 bucket as the manifest publish. It needs the POSTHOG_API_KEY secret and the POSTHOG_PROJECT_ID variable in the production environment, alongside the Cloudflare credentials publish.yml already uses. Verified against a stub PostHog endpoint: the request carries the bearer key and HogQL body, unusable rows are dropped, and the emitted document parses with BB's own `parseMarketplaceStatsJson`. Confirmed an empty result set fails without writing, and that `npm run build` still composes the manifest unchanged. > AGENT GENERATED
SawyerHood
added a commit
to get-bb/bb
that referenced
this pull request
Aug 25, 2026
## 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_installed` telemetry event (`apps/server/src/services/system/telemetry.ts`) since the store shipped, carrying a `plugin_id` for bundled plugins and `bb-community` entries — but nothing ever read it back. The curated marketplace now publishes those counts as a `stats.json` sidecar 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_marketplaces`** The column, nullable, plus migration `0107`. `statsJson` is required on `UpsertPluginMarketplaceInput` rather 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 other `ALTER TABLE ADD` in that suite is handled. **2. `db: record the drizzle snapshot for 0107`** **Skippable** — 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.ts` against the newest snapshot in `meta/`. Without it, the next person to run `db:generate` diffs against 0106, does not see `stats_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 sidecar`** A 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 search`** The 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. `installs` joins 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 counts`** Store card footer (compact `4.2K installs`, exact number in the `title`), mobile browse subtitle, and an `Installs` column in `bb 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 sidecar`** Plan doc gets the format and the reasoning; the guide chapter and bb-cli skill get the user-facing half, since `bb plugin search` grew a column. No `HOST_DAEMON_PROTOCOL_VERSION` bump: nothing here crosses the server/daemon boundary. `apps/web` needed no change — the R2 route already serves any key under `/marketplace/v1/` and gives `.json` the 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: - counts land on both curated entries and bundled plugins - an entry the sidecar does not name stays uncounted (not zero) - **the sidecar is re-read while the manifest answers 304** — the core reason for the split - a failed sidecar fetch keeps the stored counts and the refresh still records success - a malformed document (negative count) is rejected whole - a third-party marketplace's entries stay null and its `stats.json` is never requested Plus 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 typecheck` clean across all 75 tasks **at each of the six commits individually**. `test` green 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 `parseMarketplaceStatsJson` in 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 untracked `plugins/` directory without a `package.json`) and occasional 5s timeouts in `FilePreview` / `update-resolver` / `timeline-in-turn-window` under concurrent full-suite load — each passes in isolation. Fixes # > AGENT GENERATED --------- Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
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
BB servers have sent a
plugin_installedtelemetry event since the store shipped, but nothing ever read it back. The registry had no way to tell anyone how many people use a listing, and the store card had no popularity signal at all.What changed
scripts/build-stats.mjs(new,npm run build:stats) queries PostHog's HogQL API forcount(DISTINCT distinct_id)ofplugin_installedgrouped byproperties.plugin_idand writesdist/stats.json.--printwrites to stdout for local inspection..github/workflows/stats.yml(new) runs it daily plus on demand and uploads tobb-marketplace/stats.jsonin the same bucket as the manifest publish.Counts are a separate document rather than a field in
marketplace.jsonon purpose: that schema is strict, so an unknown field there rejects the whole catalog on an older desktop, and the numbers move daily while the manifest sits unchanged behind a 304.Deploy requirement: the
productionenvironment needs a newPOSTHOG_API_KEYsecret (a personal API key with project read access) and aPOSTHOG_PROJECT_IDvariable. SetPOSTHOG_HOSTif the project is not on US cloud. Until they exist the scheduled job fails and uploads nothing; the manifest publish is unaffected.The consuming side is get-bb/bb: the server fetches this file on every catalog refresh and renders the number in the store, on mobile, and in
bb plugin search.How you verified
generatedAt.parseMarketplaceStatsJsonin a scratch test in the bb checkout — it parses and reports the right count.dist/stats.json.npm run buildstill composes the manifest unchanged (63 entries).Not run: the workflow itself, which needs the credentials above.
Fixes #