Two frustrations
The R Observatory started from two frustrations. I wanted to see what was happening across CRAN and Bioconductor, the two big R package repositories, week to week: what was being published, what was breaking, what was getting pulled, and how any given package was really doing. And I wanted to look at all of it in something built this decade, not the pages the repositories serve today.
The usual answer to the first is one number: a package’s download count from the cloud CRAN mirror, the one Posit maintains and whose logs nearly every download badge counts. It is the closest thing the community has to a shared scoreboard, and it barely says anything. It is one channel, blind to the Bioconductor installs and the ones that arrive through r2u and the c2d4u PPAs, conda-forge and bioconda, and Linux distribution builds on Fedora and openSUSE. And it says nothing about the thing being downloaded: whether the code is tested, how it changed in the last release, whether a dependency three levels down is copyleft, or whether the package is one careful decade-long project or a fast-moving group effort.
The signals that would answer these questions exist, scattered across a dozen services in a dozen formats, mostly with no memory of what they looked like last month. Gathering them into one place is most of what the Observatory does. The second frustration, the look of the thing, took a different lesson to learn.
A package page, not a paint job
I have tried to fix CRAN’s pages before, from the outside. A while back I built the CSS CRAN Garden, a stylesheet you could drop in front of CRAN to make its 1990s-era HTML look like a modern site.
It works, and I still like it, but it taught me the ceiling of that approach: a stylesheet can only restyle the bytes CRAN already sends. It cannot add a download trend, a coverage number, or a version history, because CRAN never computed those in the first place. The page looks smarter without being smarter.
The R Observatory is the other half of that idea. Instead of just a reskin it adds analytics, giving anything on CRAN or Bioconductor a package page with measurement attached and, because it keeps a history, the ability to diff two versions of a package and see what changed between them.
Look it up
In practice it is a website you browse, backed by a growing pile of data that refreshes on a schedule. There is no account to create and nothing to install. It is the thing you open when you want to look something up about a package and its surroundings.
The unit you spend most of your time on is the package. For each package, and for each version of it, the Observatory measures the things you would measure by hand if you had the time: how many lines of code there are and in which languages, how many functions and how many are exported, the shape of the dependency set, test coverage, check results, and the call graph that ties the functions together. Here is what that looks like for ggplot2.
None of this is only a snapshot. Each package carries a timeline of every release it has made, with the R versions that bracket them for context and a way into the diff from any point on it.
That timeline is where the interesting part begins.
Diffing two versions
The feature that most repays keeping history is the version diff. Pick a package, pick two of its releases, and the Observatory shows you what moved between them: metadata, people, and the shape of the code, side by side. It is a changelog the maintainer never had to write.
The clearest example I know is ggplot2 from 3.0.0 to 4.0.0.
Every line of that is something you would otherwise reconstruct by hand from NEWS files, DESCRIPTION history, and git archaeology. The license row alone is the kind of thing that matters and is easy to miss: ggplot2 relicensed from GPL-2 to MIT, which is a real change in what you are allowed to do downstream, and the diff surfaces it in one line. Around it sit the parts that never make the changelog: the package roughly doubled in size, added 192 exported functions, tripled its test lines, and its copyright holder changed from RStudio to Posit.
The diff goes all the way down to the namespace. A separate panel lists every exported symbol that appeared or vanished between the releases, so an API that quietly grew, or quietly dropped a function someone depended on, is spelled out rather than left to be inferred from a count.
Downloads across the ecosystem
The downloads view is where the one-channel complaint gets its answer. Python has already been down this road, and the comparison is instructive.
In the Python world, “how many downloads” is a solved-ish problem with a known shape. PyPIStats, and its command-line client, give you the last 180 days of PyPI downloads, and when you need more you drop a level to the PyPI dataset on Google BigQuery, usually through the pypinfo CLI, which has the full history but asks you to bring a cloud account. Friendlier views sit on top of that data, from ClickHouse’s ClickPy to pepy.tech, some of them behind a paid account. But notice what it is all measuring: one index, PyPI, seen at two depths.
R has the first layer and not much of the rest. The cranlogs API is our PyPIStats: it aggregates the raw daily logs the cloud CRAN mirror publishes into a simple service, and nearly every “CRAN downloads” badge and casual citation traces back to it, the way PyPIStats sits over BigQuery. What R does not really have is the “count it everywhere” layer, because R installs are not one index. They are spread across CRAN, Bioconductor, conda, the distribution builds, and more, and no single service adds them up.
That is what the downloads view is for. Instead of a single CRAN number, installs are shown across the distribution landscape a package lives in, source by source.
The value of the combined view is the breakdown, not the grand total. The sources measure different things. A CRAN mirror hit, a conda package pull, and an r2u binary install are different events, and the page keeps them separate rather than folding them into one quantity. Read the combined number as “activity across channels,” not “unique humans.” Even with that caveat, some packages look completely different once you widen the lens. A package with a modest CRAN count can carry a large share of its installs through r2u or conda, and there is no way to see that from the CRAN counter alone.
To make that kind of gap concrete, the downloads compare view puts several packages on one chart. Line up Rcpp, V8, and reticulate on the same axis, split by source, with each package’s releases and the major R versions drawn in as reference lines, and the differences in scale and rhythm are hard to miss.
Code, colored by coverage
The Observatory does not stop at the package boundary. It goes into the code itself. For any package with tests, it builds a call graph where every function is a node, and tints each node by its test coverage on a red-to-green scale. Untested, complicated code does not hide in a table; it lights up red.
The point is not to shame anyone; a red node is often a deliberate choice, like a plotting helper or an interactive function that is hard to test. The point is that “81 percent coverage” and “this specific, central, complicated function has no tests” are very different facts, and only one of them fits in a badge.
The graph is a way in, not only a picture. Select any node and a panel opens with that function’s line count, cyclomatic complexity, coverage, and the functions it calls, so “something in here is untested” turns into a named function and its neighborhood in one click.
Dependencies, and what they oblige
Depending on a package means depending on its license, and its dependencies’ licenses, all the way down. The dependency-licenses view walks the whole install tree and gives you a plain verdict: whether anything copyleft is hiding under a permissively licensed package you thought was safe to vendor.
The same machinery powers the reverse view. Every package page knows its reverse dependencies, direct and transitive, so you can see not just what a package needs but who would feel it if the package broke. For something like ggplot2, that blast radius is most of the tidyverse and a good deal beyond it.
Who wrote it
Packages are written by people, and people show up across many packages. Every author gets a page, and every author gets a co-authorship network: a graph of everyone they have shared a package byline with, with heavier lines for more shared work.
Bioconductor, on its own terms
Everything so far has been CRAN. Half the point of the Observatory is that it does not stop there. Bioconductor is the other big R repository, and it is not just CRAN with different packages. It runs on a twice-a-year release cycle, sorts its packages into software, annotation, experiment, and workflow categories, and files each one under the hierarchical biocViews topic tree. Treating it like a CRAN clone would lose all of that, so the Observatory tracks it in its native vocabulary.
A package page speaks Bioconductor too. Instead of a plain version history it shows a release lineage: which of Bioconductor’s releases the package has appeared in, from the one it entered to the current one. apeglm, a Bayesian shrinkage estimator that DESeq2 leans on, has turned up in 18 of Bioconductor’s 49 releases since it entered at release 3.6 in 2017. Its biocViews topics place it in the tree, its dependencies are tagged by origin so the Bioconductor ones stand apart from the CRAN ones, and its code is read the same way a CRAN package’s is: a call graph down to the compiled C++ routines it drops into, with function and complexity counts beside it.
Line coverage is the one measure that does not carry over, since the Observatory does not run Bioconductor’s test suites, so those nodes fall back to a neutral fill rather than a coverage color. Code intelligence like this is still rolling out across the catalog. But the shape is right: Bioconductor is watched as Bioconductor.
The parts that move
The rest of the Observatory is about the ecosystem as a moving thing rather than a catalog, and the views that watch it try to be analytical rather than just lists.
The submission queue is the clearest example. It shows what is on its way onto CRAN, but instead of a flat list it sorts the incoming packages into stages, from initial review through human review, author action, testing, and publication. A package-flow matrix, built from months of hourly snapshots, shows where packages tend to move next from each stage, so forward progress and being sent back for revision become separate, countable things. A wait-time panel buckets everything by how long it has sat. You see not just what is in the queue but how the queue behaves.
The queue’s deeper history is borrowed, and gladly. Those hourly snapshots have been collected for years by cransays, an r-hub project whose dashboard photographs the CRAN incoming folder every hour and keeps every frame, and it is still collecting them today. The Observatory’s earlier queue history draws on that archive, and it now takes its own hourly snapshots alongside it.
A feed streams new and updated packages as they land, and the digest turns that stream into a record: what was published, updated, and archived, summarized by month, quarter, and year, on a timeline that runs back to CRAN’s early years. It is the history made browsable, one period at a time.
The checks view tracks CRAN check status across every test platform, but it sorts by consequence rather than alphabetically. It counts new and resolved issues over the last week, and a “packages at risk” table ranks the current failures by download impact, so a check error on a package with two million installs surfaces above one on a package nobody depends on.
Packages leave, too. The archived view is the queue’s mirror image: the thousands of packages pulled from the CRAN listing over the years, each with the reason it went, whether a dependency that was archived first, a check error left unfixed, or a maintainer’s email gone dark. Those reasons, and the dates behind them, come from CRAN’s own PACKAGES.in file and its structured archival fields, which most tools never read. I owe that pointer to Rich FitzJohn and Lluís Revilla Sancho, whose writing on how and why packages get archived is what led me to it.
A removed package keeps its own page, too. It stays up with the archival banner across the top, the version it last released, the reason it went, and even the lines to install that final source from the CRAN archive by hand, since install.packages() can no longer fetch it by name.
The shape of the whole
For the aggregate rather than any one package, a site-wide statistics page pulls back to the ecosystem at once: how many packages are active or archived across CRAN and Bioconductor, how licenses, compiled code, and package age are spread across everything, and how the catalog has grown since 1999. Enough is live that it has already changed how I look things up.
Find, build, observe
The moment you look at R packages beyond CRAN’s own pages, you meet a crowded field of tools. The Observatory sits off to one side of all of them, because it is not trying to do what they do.
Most of that field is about finding and reading packages. METACRAN, at r-pkg.org, is a search engine and a metadata API. RDocumentation, rdrr.io, and r-packages.io index and reformat package documentation so you can read a package without installing it. They answer “find me the package and its docs.” R-universe, from rOpenSci, is a different kind of thing: a build-and-serve platform that compiles binaries across Windows, macOS, Linux, and WebAssembly, renders documentation, produces badges, and gives every package a landing page. It answers “how do I build and document my packages.”
The Observatory does not build, host, or catalog. It watches and keeps the record: how a package is installed across every channel rather than one mirror, how its code changes from one version to the next, and how the whole ecosystem moves from one day to the next. Where the directories answer “find it” and r-universe answers “build it,” the Observatory answers “what is happening across the ecosystem, and how used and how healthy is any given package, watched over time.” They are complementary, and I use all of them.
It also leans on the neighbors’ plumbing, and owes METACRAN a particular debt. METACRAN maintains a read-only GitHub mirror of every CRAN package, with a tag for each released version, and that mirror is the raw material the Observatory walks backward through to compute per-version metrics and the version diffs. Without it the diffs would mean re-fetching every tarball by hand. The Observatory borrows a directory’s mirror, then measures what a directory never set out to measure.
A constellation of pipelines
None of this began as a plan for an observatory. It started in February 2026 with a single pipeline and the first of those frustrations: a wish to see, concretely, what was moving across the two repositories. The first public pieces went up that March, around the time I put the R mailing list archives online as a separate project. Answering even a piece of it well meant scraping one channel, then another, then most of them. Measuring one package’s code turned into a per-version pass over all of them. Each new signal, the feed, the queue, the check history, the datasets, the co-authorship graph, started as its own small job built to make one specific thing visible.
The architecture is a direct consequence of that order of events, and it is most of why the whole thing stays maintainable by one person. Each data stream is its own small pipeline, a separate repository under the r-observatory organization, and each does exactly one job. A pipeline runs on a schedule and publishes its output not to a database server but as a plain SQLite file attached to a dated GitHub release. Those releases are immutable, so each day’s data stays exactly as it was, and the history is preserved by construction rather than by remembering to back anything up. A merge step stitches the current release of each pipeline into one combined database, and the website reads that. Because every input is a self-contained, versioned file, a pipeline can fail, or be rewritten, or be added, without the others noticing, and you can always point at the exact release that produced a given number. It is less a monolith than a constellation of small, independent observers that happen to publish to the same sky.
There is a happy side effect to building it this way: the data is already open. Every number on the site traces back to one of those plain SQLite files on a public GitHub release, and nothing stops you from downloading them and working with them however you like, in R or anywhere else. There is no API to call, by design; the versioned, immutable files are the interface, and they will outlast any endpoint I could promise to keep running. Think of it as a small gift back to the R community, which wrote the packages all of this is watching in the first place.
How current the data is
Each of those pipelines is only as trustworthy as its last successful run, so the freshness page lays them out side by side: when each last ran, when it last changed the underlying data, how far that data now reaches, and whether it counts as current, slow, or stale. When something upstream stalls, this is where it surfaces first.
On the ads
You will see ads on the Observatory, and I would rather be upfront about why. The site does a lot of work: it scrapes many download sources on a schedule, computes per-version code metrics over the whole archive, and stores the history so none of it has to be recomputed. That is compute, storage, and bandwidth, and the site is free, with no account, no paywall, and nothing sold about whoever is reading it. The ads offset the running cost so it can stay that way, and they are kept out of the path of the data.
The settings are turned down as far as AdSense allows, the fewest units it permits (six) and the widest spacing between them (1,000 pixels). Even so, its automatic placement is not always ideal, so if an ad lands somewhere it should not, let me know.
If you run an ad blocker, the site works fine. Every screenshot in this post was taken with one. And if the project ever outgrows what display ads can cover, sponsorship would be a happier answer. The other route would be to put it behind accounts and a subscription, the way pepy.tech and some of the Python download sites do, but I would rather keep it free and open. For now it is one person keeping the lights on for something that is expensive to run and free to use.
The rough edges
None of this is done. Some pipelines are still backfilling history, so a few longitudinal views are shorter than they will be. A couple of download sources are wired up but not yet counted. The design is very much still moving. It is an observatory in the literal sense: a place to watch from, built out one instrument at a time.
Fin
The thread through all of it is observation. CRAN and Bioconductor tell you a package exists, and, if you go digging, a little about its latest release. The Observatory tries to answer the questions behind that: how much a package is really used across every channel, how healthy its code is, what changed between its last two releases, who wrote it, and how the two repositories move week to week. It already answers questions I had gotten used to giving up on.
It lives at r-observatory.thecoatlessprofessor.com, and the pipelines that feed it are open on GitHub. If you maintain a package, go look at its page, diff your last two releases, and see what its call graph looks like in red and green. If the multi-channel download picture, the version-by-version code view, or a Bioconductor package’s release lineage surprises you, that surprise is the point.








































